KPI rollout playbook
Use this sequence after you have Postgres, Google SSO, and the Skyfall dashboard running locally or on Vercel. The dashboard is Facebook-first: live spend and traffic from Meta, with GHL powering qualified and won counts for CPL, CPQL, and CPA.
What the dashboard shows
Open /dashboard after sign-in. Pick a From / To date range — all live tiles refresh from the APIs.
- Facebook ad spend — total spend, impressions, and clicks from the Meta Marketing API.
- Traffic KPIs — clicks, CTR, CPC, and CPM calculated from Insights in your selected range.
- CPL — Facebook spend ÷ Meta lead actions (
lead/onsite_conversion.lead_grouped). - CPQL — spend ÷ contacts in pipeline stage
LEAD QUALIFIED(first pipeline in GHL). - CPA — spend ÷ contacts tagged
won (completed)orwon (closed). Wins are usually applied manually in GHL when a deal closes. - Campaign spend — top campaigns by spend with click and CTR breakdown.
Sync Facebook on the dashboard stores a 7-day Meta snapshot in Postgres (sync history). Live tiles do not require Postgres; only sync history and optional DB banners do.
Environment variables
Copy .env.example → .env.local in the project root.
AUTH_SECRET— random string, ≥ 32 characters (NextAuth signing).AUTH_URL—http://localhost:3000locally; production URL on Vercel.AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET— Google OAuth web client.DATABASE_URL— Postgres (local Docker or hosted). Rundocker compose up -d && npm run db:pushfor local.CRON_SECRET— bearer token forGET /api/cron/sync.META_ACCESS_TOKEN— long-lived token withads_read.META_AD_ACCOUNT_ID— ad account id (123456789oract_123456789).GHL_PRIVATE_INTEGRATION_TOKEN— GHL Private Integration token (required for CPQL/CPA).GHL_LOCATION_ID— sub-account Location ID.GHL_CLOSED_DEAL_TAG(optional) — comma-separated won tags; defaults towon (completed),won (closed).
Google SSO
- Google Cloud Console → Credentials → Create OAuth client ID → Web application.
- Authorized JavaScript origins:
http://localhost:3000and your production origin. - Redirect URIs:
/api/auth/callback/googleon each origin. - Only @esntlsystems.com workspace accounts can sign in (enforced in
src/auth.ts).
Meta Ads (Facebook KPIs)
- Meta Business Suite → confirm access to the ad account you want (note the
act_…id in URLs). - Create a long-lived access token with
ads_read(Graph Explorer short-lived token → exchange, or System User token for agencies). Never commit tokens to git. - Set
META_ACCESS_TOKENandMETA_AD_ACCOUNT_IDin.env.local(and Vercel env for production). - Live range tiles call
GET /v21.0/act_…/insightswith your From/To dates. Sync usesdate_preset=last_7dand stores spend / impressions / clicks in Postgres. - If tiles show a token error, generate a new long-lived token and update
META_ACCESS_TOKEN— then restart dev or redeploy.
GoHighLevel (CPQL & CPA)
- GHL → Settings → Integrations → Private Integration → copy token into
GHL_PRIVATE_INTEGRATION_TOKEN. - Copy your sub-account Location ID into
GHL_LOCATION_ID. - The token needs scopes for contacts search, opportunities, and tags (e.g.
tags.readonly). - CPQL counts opportunities in stage
LEAD QUALIFIEDon the first pipeline returned by the API, filtered by your date range. - CPA counts contacts whose tags match
won (completed)orwon (closed)usingcontains_setsearch. Many accounts usewon (completed)only — setGHL_CLOSED_DEAL_TAGif yours differs. - Won deals are typically tagged manually when a lead closes (e.g. stage LEAD VEHICLE PICKED UP WITH CLAIM). If date-filtered tag search returns 0, the dashboard falls back to all-time tag counts and shows a note on the funnel.
Google Tag Manager (optional)
- Export a GTM container backup for documentation — list which tags fire (Meta Pixel, chat widgets, etc.). Skyfall does not ingest GA4 or Google Ads; use GTM as your tag governance map.
- Keep one primary on-site measurement path so internal reporting does not double-count leads.
- Optional: push structured
dataLayerevents for form steps and chat opens on landing pages.
Hyros (optional)
- Confirm what your Hyros plan exposes (API, exports, webhooks).
- Pragmatic v1: deep-link to Hyros for attribution detail; ingest summaries via export or webhook into your DB later.
- Map Hyros revenue definitions to GHL stages so leadership sees one story.
Website + server events
- Verify production lead endpoints (e.g.
/api/meta/lead,/api/hyros/lead) return 200 on test submits if you use them on landing pages. - Optionally POST anonymized lead events into Postgres for ground-truth counts alongside Meta lead actions.
Cron on production
- Schedule
GET /api/cron/syncwith headerAuthorization: Bearer CRON_SECRET(Vercel Cron, GitHub Actions, etc.). - Never expose
CRON_SECRETin the browser or client-side code. - Local test:
curl -sS -H "Authorization: Bearer $CRON_SECRET" "http://localhost:3000/api/cron/sync"
Deploy to skyfall.agency (Vercel + Cloudflare)
Skyfall is one Next.js app: marketing at skyfall.agency, dashboard at dashboard.skyfall.agency. Keep Cloudflare for DNS on your domain; host the app on Vercel (API routes, Google login, Prisma, hourly cron). Do not use Docker on Vercel — use hosted Postgres instead.
Before deploy, run locally: npm run verify:production (checks env vars; production DATABASE_URL must not be localhost).
A. Production database (Neon)
- Create a project at neon.tech and copy the Postgres connection string (
sslmode=require). - One-time schema push from your Mac:
DATABASE_URL="postgresql://…neon…" npx prisma db push - Set the same
DATABASE_URLin Vercel → Settings → Environment Variables.
B. Vercel project
- Push this repo to GitHub (root folder:
dashboard-skyfallif monorepo). - vercel.com → Import repo → set Root Directory to
dashboard-skyfallif needed. - Add all env vars from
.env.localfor Production (never commit secrets). SetAUTH_URLtohttps://dashboard.skyfall.agency. - Include your long-lived
META_ACCESS_TOKEN— not App Secret (App Secret stays local fornpm run meta:long-tokenonly).
C. Google OAuth (both domains)
- Authorized JavaScript origins:
https://skyfall.agency,https://dashboard.skyfall.agency - Redirect URIs:
https://skyfall.agency/api/auth/callback/google,https://dashboard.skyfall.agency/api/auth/callback/google
D. Cloudflare DNS → Vercel
In Cloudflare for skyfall.agency, add domains in Vercel first, then create records:
- skyfall.agency — CNAME
@or use Vercel's A record → target shown in Vercel Domains (oftencname.vercel-dns.com) - dashboard.skyfall.agency — CNAME
dashboard→ same Vercel project - SSL/TLS mode: Full (not Flexible) when proxying to Vercel
Cloudflare Workers/Pages can stay for other skyfall.agency assets later; this app runs on Vercel because it needs Next.js server routes, NextAuth, Prisma, and cron.
E. Verify production
https://skyfall.agency— marketing site loadshttps://dashboard.skyfall.agency/login— Google sign-in workshttps://dashboard.skyfall.agency/dashboard— Facebook KPI tiles show spend- Vercel → Cron Jobs →
/api/cron/syncreturns 200 hourly