A dashboard where each client logs in and sees only their own Meta Ads lead performance data (spend, leads, clicks, impressions, cost per lead) — replacing the daily manual WhatsApp screenshot process.
Pilot tenant: Excellanz Education (Meta ad account 2011227639500053).
better-sqlite3 (data/app.db, committed to the repo)iron-session, passwords hashed with bcryptjs/login — shared login for all clients + the internal team/[client-slug]/dashboard — a client’s own view (summary cards, campaign
breakdown table, daily cost-per-lead trend). Enforced server-side: a
session can only ever read the client it belongs to, regardless of the
slug in the URL./admin — internal VR Digitals team view across all clients (not
reachable by client sessions)npm install
cp .env.local.example .env.local # then fill in SESSION_SECRET
SESSION_SECRET must be a random string of 32+ characters, e.g.:
openssl rand -hex 32
Seed the pilot tenant and an internal team login:
EXCELLANZ_PASSWORD="choose-a-password" TEAM_PASSWORD="choose-a-password" npm run seed
Run the dev server:
npm run dev
clients — client_slug, display_name, meta_ad_account_idusers — username, password_hash (bcrypt), role (client | team),
client_id (null for team users)daily_data — one row per (client, date, campaign): spend, leads,
clicks, impressions, reach, cost_per_leadSee lib/db.ts for the schema and all data-access functions.
Every read is scoped by client_id derived from the session — never from a
client-supplied slug or id.
npm run upsert-daily-data -- path/to/data.json
data/app.db and pushes to GitHubThis is configuration, not code:
upsertClient (slug, display name, ad account id)upsertUser (bcrypt-hashed password, role: "client",
linked client_id)See scripts/seed.ts for the pattern.