Full stack plan for a campaign reporting dashboard that stays simple to operate. Built to ingest public post metrics through n8n and ScrapeCreators, track committed-vs-actual KPIs, and ship a polished read-only view for clients.
Every architectural choice below serves one of these five outcomes.
Seven fully designed screens, all linked in the navigation hub.
| Screen | Purpose | Route |
|---|---|---|
| Campaign List | All campaigns at a glance, select 2+ to compare | /campaigns |
| Campaign Detail | Hero screen - KPIs, daily growth, platform split, influencer grid | /campaigns/:id |
| Influencer Detail | Per-KOL deep dive - performance, real public posts, contract | /influencers/:id |
| Alerts Center | Triage under-KPI / engagement-drop / deadline alerts | /alerts |
| Compare | Side-by-side for 2-3 campaigns with winners & CPM | /compare?ids=... |
| Client View | Read-only shareable link - no sidebar, no edit actions | /share/:token |
| Architecture Doc | This page | /docs/architecture |
?api=1 để đọc/ghi qua backend tối thiểu.
Three loops run continuously. The dashboard is just a view over the state they produce.
metrics_hourly (Timescale hypertable). We never overwrite - history is gold for the Daily Growth chart.campaign_daily and influencer_daily materialized views.metrics_hourly directly from the UI. That table can hit millions of rows; aggregates stay under 10k.kpi_snapshots stores the "right now" %: actual / committed for views, engagement, posts. One row per influencer × campaign, updated after every aggregation pass.50%, 80%, 95%. If KPI is behind that milestone, alert severity rises.alerts with a debounce key (no duplicate alerts within 24h).Only the essentials - expand as you go.
-- CORE ENTITIES CREATE TABLE organizations ( id UUID PRIMARY KEY, name TEXT, created_at TIMESTAMPTZ ); CREATE TABLE users ( id UUID PRIMARY KEY, org_id UUID REFERENCES organizations, email CITEXT UNIQUE, role TEXT -- owner | manager | analyst ); CREATE TABLE clients ( id UUID PRIMARY KEY, org_id UUID, name TEXT, brand_color TEXT, logo_url TEXT ); CREATE TABLE campaigns ( id UUID PRIMARY KEY, org_id UUID, client_id UUID, name TEXT, status TEXT, -- draft | live | ended start_date DATE, end_date DATE, budget_cents BIGINT, committed_views BIGINT, committed_engagement BIGINT ); CREATE TABLE influencers ( id UUID PRIMARY KEY, display_name TEXT, avatar_url TEXT, tier TEXT -- nano | micro | mid | macro | mega ); CREATE TABLE influencer_handles ( id UUID PRIMARY KEY, influencer_id UUID, platform TEXT, -- tiktok | instagram | facebook handle TEXT, platform_user_id TEXT, followers BIGINT ); -- THE BOOKING - a KOL committed to a campaign CREATE TABLE bookings ( id UUID PRIMARY KEY, campaign_id UUID, influencer_id UUID, fee_cents BIGINT, committed_views BIGINT, committed_engagement BIGINT, committed_posts INT, min_engagement_rate NUMERIC(5,2), exclusivity_days INT, usage_rights_days INT ); -- POSTS - each booked piece of content CREATE TABLE posts ( id UUID PRIMARY KEY, booking_id UUID, platform TEXT, platform_post_id TEXT, url TEXT, thumb_url TEXT, type TEXT, -- video | reel | feed | live | short caption TEXT, pull_status TEXT, error_message TEXT, posted_at TIMESTAMPTZ, last_pulled_at TIMESTAMPTZ, source TEXT ); -- TIME-SERIES (Timescale hypertable) CREATE TABLE metrics_hourly ( post_id UUID, ts TIMESTAMPTZ, views BIGINT, likes BIGINT, comments BIGINT, shares BIGINT, saves BIGINT, reach BIGINT ); SELECT create_hypertable('metrics_hourly', 'ts'); -- DERIVED SNAPSHOTS (updated by Loop 2) CREATE TABLE kpi_snapshots ( booking_id UUID PRIMARY KEY, actual_views BIGINT, actual_engagement BIGINT, actual_posts INT, kpi_pct NUMERIC(5,2), engagement_rate NUMERIC(5,2), updated_at TIMESTAMPTZ ); -- ALERTS CREATE TABLE alerts ( id UUID PRIMARY KEY, campaign_id UUID, booking_id UUID, type TEXT, -- under_kpi | engagement_drop | deadline | sentiment severity TEXT, message TEXT, status TEXT, raised_at TIMESTAMPTZ, resolved_at TIMESTAMPTZ ); -- SHARE LINKS CREATE TABLE share_links ( token TEXT PRIMARY KEY, -- 32-char random campaign_id UUID, created_by UUID, expires_at TIMESTAMPTZ, password_hash TEXT, last_viewed_at TIMESTAMPTZ, view_count INT DEFAULT 0 );
Current REST shape for the pilot backend. Without ?api=1, the front-end still uses localStorage as a safe demo fallback. Admin CRUD currently persists through the full snapshot endpoint; granular create/update endpoints can be added after the pilot schema is stable.
| Method | Route | Notes |
|---|---|---|
| GET | /api/health | Backend readiness and store mode |
| GET | /api/pulse | Full PULSE snapshot for the front-end API adapter |
| PUT | /api/pulse | Persist Admin changes from API mode into the pilot backend store |
| GET | /api/campaigns | Campaign list for dashboard views |
| GET | /api/campaigns/:id | Single campaign record |
| GET | /api/campaigns/:id/bookings | Bookings for campaign detail and influencer detail |
| GET | /api/campaigns/:id/posts | Public post metrics pulled through n8n/ScrapeCreators |
| GET | /api/campaigns/:id/daily | Daily growth series |
| GET | /api/alerts | Current stale-data / KPI alert rows |
| POST | /api/alerts/:id/resolve | Mark resolved |
| POST | /api/auth/email-link | Send a Firebase email sign-in link through the backend mailer |
| POST | /api/ingest/posts | n8n sends normalized ScrapeCreators payloads to backend |
| GET | /api/share/:token | Client read-only campaign payload; hides fees/costs/internal fields |
| POST | /api/client-invites | Create hashed client share token and send invite email when auth/email are configured |
| GET | /api/media-proxy | Server-side thumbnail proxy for API/production mode |
Three layers - each cheaper than the last.
| Event | Delay to dashboard |
|---|---|
| Post URL added to a booking | Next n8n scheduled run or manual run |
| View count update on TikTok post | Next ScrapeCreators/n8n pull window (5-6/day) |
| Campaign crosses 80% timeline | After next payload sync + aggregation |
| New alert raised | Immediately after aggregation in the UI |
share_links.password_hash.org_id; the share endpoint is the only public surface and it resolves the campaign scope from the token server-side.6 weeks to v1 with one full-stack engineer + one designer.
| Week | Ship | Key risk |
|---|---|---|
| 1 | Auth, org/client/campaign CRUD, CSV import for metrics | None - scaffolding |
| 2 | Campaign Detail v1 with mocked metrics, design system finalized | Design decisions |
| 3 | n8n workflow family, ScrapeCreators payload import, provider error logging | Quota and missing post URLs |
| 4 | TikTok, Instagram, Facebook post metrics + alerting engine | Rate limits and missing post URLs |
| 5 | Client share links, PDF export, compare view | PDF rendering fidelity |
| 6 | Polish, internal QA, beta with 1 friendly client | Client expectations - under-promise real-time |