Skyfall
← Back to dashboardRollout guide

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.

Step 1

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) or won (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.

Step 2

Environment variables

Copy .env.example.env.local in the project root.

  • AUTH_SECRET — random string, ≥ 32 characters (NextAuth signing).
  • AUTH_URLhttp://localhost:3000 locally; production URL on Vercel.
  • AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET — Google OAuth web client.
  • DATABASE_URL — Postgres (local Docker or hosted). Run docker compose up -d && npm run db:push for local.
  • CRON_SECRET — bearer token for GET /api/cron/sync.
  • META_ACCESS_TOKEN — long-lived token with ads_read.
  • META_AD_ACCOUNT_ID — ad account id (123456789 or act_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 to won (completed),won (closed).
Step 3

Google SSO

  1. Google Cloud Console → Credentials → Create OAuth client ID → Web application.
  2. Authorized JavaScript origins: http://localhost:3000 and your production origin.
  3. Redirect URIs: /api/auth/callback/google on each origin.
  4. Only @esntlsystems.com workspace accounts can sign in (enforced in src/auth.ts).
Step 4

Meta Ads (Facebook KPIs)

  1. Meta Business Suite → confirm access to the ad account you want (note the act_… id in URLs).
  2. 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.
  3. Set META_ACCESS_TOKEN and META_AD_ACCOUNT_ID in .env.local (and Vercel env for production).
  4. Live range tiles call GET /v21.0/act_…/insights with your From/To dates. Sync uses date_preset=last_7d and stores spend / impressions / clicks in Postgres.
  5. If tiles show a token error, generate a new long-lived token and update META_ACCESS_TOKEN — then restart dev or redeploy.
Step 5

GoHighLevel (CPQL & CPA)

  1. GHL → Settings → Integrations → Private Integration → copy token into GHL_PRIVATE_INTEGRATION_TOKEN.
  2. Copy your sub-account Location ID into GHL_LOCATION_ID.
  3. The token needs scopes for contacts search, opportunities, and tags (e.g. tags.readonly).
  4. CPQL counts opportunities in stage LEAD QUALIFIED on the first pipeline returned by the API, filtered by your date range.
  5. CPA counts contacts whose tags match won (completed) or won (closed) using contains_set search. Many accounts use won (completed) only — set GHL_CLOSED_DEAL_TAG if yours differs.
  6. 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.
Step 6

Google Tag Manager (optional)

  1. 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.
  2. Keep one primary on-site measurement path so internal reporting does not double-count leads.
  3. Optional: push structured dataLayer events for form steps and chat opens on landing pages.
Step 7

Hyros (optional)

  1. Confirm what your Hyros plan exposes (API, exports, webhooks).
  2. Pragmatic v1: deep-link to Hyros for attribution detail; ingest summaries via export or webhook into your DB later.
  3. Map Hyros revenue definitions to GHL stages so leadership sees one story.
Step 8

Website + server events

  1. Verify production lead endpoints (e.g. /api/meta/lead, /api/hyros/lead) return 200 on test submits if you use them on landing pages.
  2. Optionally POST anonymized lead events into Postgres for ground-truth counts alongside Meta lead actions.
Step 9

Cron on production

  1. Schedule GET /api/cron/sync with header Authorization: Bearer CRON_SECRET (Vercel Cron, GitHub Actions, etc.).
  2. Never expose CRON_SECRET in the browser or client-side code.
  3. Local test: curl -sS -H "Authorization: Bearer $CRON_SECRET" "http://localhost:3000/api/cron/sync"
Step 10

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)

  1. Create a project at neon.tech and copy the Postgres connection string (sslmode=require).
  2. One-time schema push from your Mac: DATABASE_URL="postgresql://…neon…" npx prisma db push
  3. Set the same DATABASE_URL in Vercel → Settings → Environment Variables.

B. Vercel project

  1. Push this repo to GitHub (root folder: dashboard-skyfall if monorepo).
  2. vercel.com → Import repo → set Root Directory to dashboard-skyfall if needed.
  3. Add all env vars from .env.local for Production (never commit secrets). Set AUTH_URL to https://dashboard.skyfall.agency.
  4. Include your long-lived META_ACCESS_TOKEN — not App Secret (App Secret stays local for npm run meta:long-token only).

C. Google OAuth (both domains)

  1. Authorized JavaScript origins: https://skyfall.agency, https://dashboard.skyfall.agency
  2. 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 (often cname.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 loads
  • https://dashboard.skyfall.agency/login — Google sign-in works
  • https://dashboard.skyfall.agency/dashboard — Facebook KPI tiles show spend
  • Vercel → Cron Jobs → /api/cron/sync returns 200 hourly