Dashboard Deployment Guide

Status: Active Owner: @bilal Last Updated: 2026-02-21

First-Time Setup Checklist

Complete these steps in order before the first deploy. Each step depends on the previous one.

Step 1: Terraform Cloud Workspace

Create the dashboard-prod workspace in Terraform Cloud.

Organisation: envo-energy
Workspace:    dashboard-prod
Workflow:     CLI-Driven

No workspace variables needed — credentials are passed via TF_VAR_* env vars from GitHub Actions secrets (same as ehq-brain). The team_emails variable has defaults in the Terraform code.

Step 2: Terraform Apply

cd infra/dashboard/
terraform init
terraform plan    # Verify: custom domain + Access app + 2 policies
terraform apply

This creates:

  • Workers custom domain app.ehq.tech
  • Cloudflare Access application (ZTNA gate)
  • Allow policy for team emails + deny-all policy

Step 3: Hyperdrive Config

Create a Hyperdrive config pointing to the Supabase direct connection (port 5432, not pooled 6543):

cd envo-dashboard/
npx wrangler hyperdrive create envo-db \
  --connection-string="postgresql://postgres.cslpfplavhdkfprrmwno:<password>@db.cslpfplavhdkfprrmwno.supabase.co:5432/postgres"

Copy the returned config ID, then edit wrangler.jsonc:

  1. Uncomment the "hyperdrive" block
  2. Set the "id" to the returned value
  3. The "localConnectionString" is already set for local Supabase

Step 4: Set Non-Secret Vars

Edit wrangler.jsonc — uncomment and fill the "vars" block:

"vars": {
  "NEXT_PUBLIC_SUPABASE_URL": "https://cslpfplavhdkfprrmwno.supabase.co",
  "NEXT_PUBLIC_SUPABASE_ANON_KEY": "<production-anon-key>",
  "SENDGRID_FROM_EMAIL": "notifications@ehq.tech"
}

Step 5: Set Worker Secrets

cd envo-dashboard/
 
# Auth
npx wrangler secret put SUPABASE_SERVICE_ROLE_KEY
 
# AI
npx wrangler secret put ANTHROPIC_API_KEY
npx wrangler secret put OPENAI_API_KEY
 
# Twilio
npx wrangler secret put TWILIO_ACCOUNT_SID
npx wrangler secret put TWILIO_AUTH_TOKEN
npx wrangler secret put TWILIO_SMS_NUMBER
npx wrangler secret put TWILIO_WHATSAPP_NUMBER
 
# Meta WhatsApp (if using Meta direct API)
npx wrangler secret put META_WHATSAPP_PHONE_NUMBER_ID
npx wrangler secret put META_WHATSAPP_ACCESS_TOKEN
npx wrangler secret put META_WHATSAPP_VERIFY_TOKEN
npx wrangler secret put META_WHATSAPP_APP_SECRET
 
# Email
npx wrangler secret put SENDGRID_API_KEY
 
# Voice
npx wrangler secret put VOICE_WEBHOOK_SECRET

Each command prompts for the value interactively (never passed on command line).

Step 6: GitHub Actions Secrets

These are set in the repo’s production environment (Settings > Environments > production). Most should already exist from the brain deployment.

Secrets (already set from brain deployment unless noted):

SecretValueNotes
CLOUDFLARE_API_TOKENCF API tokenAlready set (verify it has Workers Scripts + Workers Routes permissions)
CLOUDFLARE_ACCOUNT_IDCF account IDAlready set
TF_API_TOKENTerraform Cloud tokenAlready set
SUPABASE_ACCESS_TOKENSupabase access tokenNew. From Supabase Dashboard → Account → Access Tokens

Variables (non-secret):

VariableValueNotes
SUPABASE_PROJECT_REFcslpfplavhdkfprrmwnoNew. Used by supabase db push in CI
NEXT_PUBLIC_SUPABASE_URLhttps://cslpfplavhdkfprrmwno.supabase.coNew. Build-time env for Next.js
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anon keyNew. Build-time env for Next.js

Step 7: First Deploy

cd envo-dashboard/
pnpm build:cf      # Build for Cloudflare Workers (~5 MiB compressed)
pnpm deploy:cf     # Deploy to Cloudflare Workers

Step 8: Verify

  1. Visit app.ehq.tech — should show Cloudflare Access login page
  2. Enter your email — receive OTP code
  3. After OTP — Supabase Auth login page loads
  4. Log in — dashboard loads, GraphQL queries work

What Lives Where

Cloudflare (app.ehq.tech)

  • Workers: Next.js app via @opennextjs/cloudflare
  • Hyperdrive: Connection pool to Supabase Postgres (direct connection, port 5432)
  • Access (ZTNA): Email OTP gate for team members
  • DNS: Workers custom domain for app.ehq.tech

Cloudflare (brain.ehq.tech)

  • Pages: Quartz static knowledge base
  • Access (ZTNA): Email OTP gate for team members

Supabase Cloud

  • PostgreSQL 16 + pgvector (database)
  • Supabase Auth (user authentication)
  • Supabase Storage (file uploads)

Terraform Cloud

  • Workspace: dashboard-prod (infra/dashboard/)
  • Workspace: ehq-brain (infra/brain/)

Day-to-Day Operations

Manual Deploy

cd envo-dashboard/
pnpm build:cf         # Build for Cloudflare Workers
pnpm deploy:cf        # Deploy to Cloudflare Workers

Local Preview (CF runtime)

Requires Hyperdrive localConnectionString set in wrangler.jsonc and local Supabase running:

cd envo-dashboard/
pnpm build:cf
pnpm preview:cf       # Runs on workerd locally

CI/CD (automated)

Pushes to main that touch envo-dashboard/** trigger deploy-dashboard.yml:

  1. pnpm install --frozen-lockfile
  2. pnpm prisma generate (generates Prisma client + Pothos types from schema.prisma)
  3. supabase db push (applies pending migrations via Supabase management API)
  4. pnpm build:cf
  5. npx wrangler deploy

Pushes to main that touch infra/dashboard/** trigger terraform-dashboard.yml:

  1. terraform plan (on PR)
  2. terraform apply (on merge to main)

ZTNA (Cloudflare Access)

  • Auth method: Email OTP (one-time passcode via Cloudflare)
  • Allowed emails: Configured via team_emails Terraform variable
  • Session duration: 7 days
  • Behaviour: All requests to app.ehq.tech hit the Access gate. After OTP, requests pass through to the Worker. Supabase Auth runs behind Access as a second layer.

Removing ZTNA (going public)

When the app is ready for public access:

  1. Delete the cloudflare_access_application and cloudflare_access_policy resources from infra/dashboard/main.tf
  2. Run terraform apply — the Access gate is removed
  3. Webhook routes (/api/tenant/*) become externally accessible as needed

Costs

Workers Paid plan: $5/month flat (required for 10 MB worker size limit).

ResourceIncludedOur usage (3 users behind ZTNA)
Requests10M/month~few thousand/day
CPU time30M ms/month~1-2M ms/month
HyperdriveUnlimitedUnlimited
BandwidthNo chargeNo charge

Architecture Notes

Bundle Size

The worker bundle is ~4.9 MiB compressed (limit: 10 MiB). Key contributor: Prisma query compiler WASM (0.72 MiB compressed). The app uses engineType: "client" in Prisma to avoid the 19 MB native binary engine.

If the bundle approaches ~8 MiB, consider splitting into separate workers:

  • api.ehq.tech — GraphQL + AI/RAG + integrations (Hono, no Next.js)
  • app.ehq.tech — Dashboard UI (Next.js, calls API worker)
  • chat.ehq.tech — Tenant chat widget

Database Connection

Cloudflare Workers cannot hold persistent TCP connections. Hyperdrive provides connection pooling:

  • Workers connect to Hyperdrive (local to CF edge)
  • Hyperdrive maintains a pool to Supabase Postgres
  • @prisma/adapter-pg + pg Pool used with engineType: "client"

Local Development

Local dev uses the standard Node.js path (pnpm dev), not the CF Workers runtime. The getDb() function in lib/db/index.ts detects the runtime:

  • Cloudflare Workers: Uses Hyperdrive connection string via @prisma/adapter-pg
  • Node.js (local): Uses global PrismaClient singleton with DATABASE_URL from .env