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 applyThis 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:
- Uncomment the
"hyperdrive"block - Set the
"id"to the returned value - 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_SECRETEach 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):
| Secret | Value | Notes |
|---|---|---|
CLOUDFLARE_API_TOKEN | CF API token | Already set (verify it has Workers Scripts + Workers Routes permissions) |
CLOUDFLARE_ACCOUNT_ID | CF account ID | Already set |
TF_API_TOKEN | Terraform Cloud token | Already set |
SUPABASE_ACCESS_TOKEN | Supabase access token | New. From Supabase Dashboard → Account → Access Tokens |
Variables (non-secret):
| Variable | Value | Notes |
|---|---|---|
SUPABASE_PROJECT_REF | cslpfplavhdkfprrmwno | New. Used by supabase db push in CI |
NEXT_PUBLIC_SUPABASE_URL | https://cslpfplavhdkfprrmwno.supabase.co | New. Build-time env for Next.js |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anon key | New. 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 WorkersStep 8: Verify
- Visit
app.ehq.tech— should show Cloudflare Access login page - Enter your email — receive OTP code
- After OTP — Supabase Auth login page loads
- 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 WorkersLocal 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 locallyCI/CD (automated)
Pushes to main that touch envo-dashboard/** trigger deploy-dashboard.yml:
pnpm install --frozen-lockfilepnpm prisma generate(generates Prisma client + Pothos types fromschema.prisma)supabase db push(applies pending migrations via Supabase management API)pnpm build:cfnpx wrangler deploy
Pushes to main that touch infra/dashboard/** trigger terraform-dashboard.yml:
terraform plan(on PR)terraform apply(on merge to main)
ZTNA (Cloudflare Access)
- Auth method: Email OTP (one-time passcode via Cloudflare)
- Allowed emails: Configured via
team_emailsTerraform variable - Session duration: 7 days
- Behaviour: All requests to
app.ehq.techhit 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:
- Delete the
cloudflare_access_applicationandcloudflare_access_policyresources frominfra/dashboard/main.tf - Run
terraform apply— the Access gate is removed - Webhook routes (
/api/tenant/*) become externally accessible as needed
Costs
Workers Paid plan: $5/month flat (required for 10 MB worker size limit).
| Resource | Included | Our usage (3 users behind ZTNA) |
|---|---|---|
| Requests | 10M/month | ~few thousand/day |
| CPU time | 30M ms/month | ~1-2M ms/month |
| Hyperdrive | Unlimited | Unlimited |
| Bandwidth | No charge | No 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+pgPool used withengineType: "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_URLfrom.env