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. If Facebook or GoHighLevel is not connected, a Get started banner walks you through setup. Pick a date range (or use 7d / 14d / 30d presets) — live tiles refresh from the APIs.

  • Hero KPI strip — Spend, Leads, Qualified, Won, CPL, CPQL, and CPA at a glance (hover for definitions).
  • Facebook ad spend — total spend, impressions, and clicks from the Meta Marketing API.
  • Traffic metrics (expand) — clicks, CTR, CPC, and CPM from Insights.
  • CPL — Facebook spend ÷ Meta lead actions (lead / onsite_conversion.lead_grouped).
  • CPQL — spend ÷ GHL opportunities from LEAD QUALIFIED through your closed stage in the selected pipeline.
  • CPA — spend ÷ contacts in your configured win pipeline stages (set in Connections when you connect GHL).
  • Conversion funnel (expand) — lead → qualified → won counts with cost per step.
  • Campaign drill-down — campaigns → ad sets → ads with CPL, CPQL, and CPA per row. A ~ prefix means CPQL/CPA was estimated from campaign-level GHL data.
  • Tracker/dashboard/tracker for pixel, postback, GHL webhook, and affiliate events (optional; not required for top-level KPI tiles).

Sync Facebook stores a Meta snapshot in Postgres (sync history). Resync CRM refreshes GHL qualified/won counts. Live tiles work without Postgres; only sync history and tracker events need the database.

Step 2

Connect Facebook & GHL

On the KPI dashboard, open Connections (right column on desktop). Complete both integrations — KPIs stay empty until Facebook and GoHighLevel are connected.

  1. Facebook — click Connect Facebook, authorize Meta, then pick your ad account. Reconnect from Connections if you see a token expiry banner.
  2. GoHighLevel — paste your Private Integration token, Location ID, pipeline name (e.g. FIRST PIPELINE), and optional CPA win stages (comma-separated stage names).
  3. Optional: expand Skyfall tracker in Connections for pixel, postback, and GHL webhook URLs. Optional: copy Meta URL params for sharper per-ad CPQL/CPA in drill-down.
Step 3

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 4

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 5

Meta Ads (Facebook KPIs)

  1. In the dashboard, use Connections → Connect Facebook (OAuth). Pick the ad account that matches the campaigns you report on.
  2. For local dev or server-side env fallback, set META_APP_ID, META_APP_SECRET, and connect via the UI — tokens are stored per user in Postgres.
  3. Live range tiles call Meta Insights with your From/To dates. If tiles show a token error, use Reconnect Facebook in Connections or the expiry banner's refresh action.
  4. Production runs a daily cron at /api/cron/meta-tokens to refresh long-lived tokens before they expire.
Step 6

GoHighLevel (CPQL & CPA)

  1. GHL → Settings → Integrations → Private Integration → copy token into Connections (or GHL_PRIVATE_INTEGRATION_TOKEN for env-based dev).
  2. Copy your sub-account Location ID and pipeline name (e.g. FIRST PIPELINE).
  3. The token needs scopes for contacts search, opportunities, and tags.
  4. CPQL counts opportunities from stage LEAD QUALIFIED through your closed/won stage in the configured pipeline, filtered by the dashboard date range.
  5. CPA counts opportunities in your CPA win stages — enter comma-separated stage names in Connections (e.g. Won, LEAD VEHICLE PICKED UP WITH CLAIM #). If unset, Skyfall uses default win-stage name matching.
  6. Use Resync CRM on the dashboard after bulk GHL stage changes. Per-ad CPQL/CPA in drill-down improves when landing URLs include Meta campaign_id / ad_id params (copy templates in Connections).
Step 7

Skyfall tracker (optional)

Tracker ingest is optional — top-level CPL, CPQL, and CPA use Meta + GHL directly. Use the tracker when you want a server-side event log or affiliate/postback attribution.

  1. In Connections → Skyfall tracker, copy your pixel snippet, track API URL, postback URL, and GHL workflow webhook URL.
  2. View events on /dashboard/tracker — filter by date, event type, and source; expand rows for full payloads.
  3. Postback and GHL webhook events can improve per-ad CPQL/CPA in campaign drill-down when GHL contact attribution includes campaign_id / ad_id.
Step 8

Cron on production

  1. GET /api/cron/sync — hourly Meta snapshot to Postgres. Header: Authorization: Bearer CRON_SECRET.
  2. GET /api/cron/meta-tokens — daily Facebook token refresh. Same CRON_SECRET header.
  3. Never expose CRON_SECRET in the browser or client-side code.
  4. Local test: curl -sS -H "Authorization: Bearer $CRON_SECRET" "http://localhost:3000/api/cron/sync"
Step 9

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 — KPI dashboard shows spend after connecting Facebook
  • Vercel → Cron Jobs → /api/cron/sync (hourly) and /api/cron/meta-tokens (daily) return 200