Self-hosting & running the SaaS
LeonAI is a TypeScript monorepo: a Fastify API, a Next.js frontend, analytics and webhook workers, and shared packages. This page covers local development and production deployment on Google Cloud Platform (GCP).
Prerequisites
- Node.js 20+
- Docker (for local MySQL 8 + Redis 7 via Docker Compose)
Local development
# One-time setup: starts MySQL + Redis, writes .env files with generated dev
# secrets, installs deps, builds shared packages, runs Prisma migrations.
npm run setup
# Start everything: API on :8080, frontend on :3000, workers in-process.
npm run dev
# Seed a demo account (API must be running).
npm run seed:demo
npm run setup is idempotent. After seed:demo you can log in with:
- Email:
demo@leonai.test - Password:
Demo12345(Business tier)
Handy local URLs:
| What | URL |
|---|---|
| Dashboard | http://localhost:3000 |
| API health | http://localhost:8080/health |
| Swagger UI | http://localhost:8787/api/v1/docs |
| Redirect | http://localhost:8080/<shortCode> |
Optional services (Sumit billing, SendGrid email, MaxMind GeoIP, Anthropic AI, Cloudflare domain auto-connect) degrade gracefully when their keys are absent — for example, AI falls back to the deterministic heuristic provider.
Environment variables
Local defaults are written by npm run setup. For production the API refuses to
boot unless the security-critical values are strong and explicit — this is enforced by
the env-guard (packages/api/src/utils/env-guard.ts).
Hard-fail in production (the API will not start)
| Variable | Requirement |
|---|---|
JWT_SECRET | Present, ≥32 chars, not a known placeholder |
COOKIE_SECRET | Present, ≥32 chars, distinct from JWT_SECRET |
CORS_ORIGIN | Present — explicit origin(s), comma-separated; no wildcard/permissive default |
Also required for a working deployment
| Variable | Purpose |
|---|---|
DATABASE_URL | CloudSQL MySQL (Unix socket on Cloud Run) |
REDIS_* (host/port/password, read+write) | Cache, rate limits, queues, sessions |
TOTP_ENCRYPTION_KEY | 32-byte hex — required for 2FA enrollment (else 503) |
DNS_ENCRYPTION_KEY | Encrypts stored Cloudflare tokens for domain auto-connect (falls back to TOTP_ENCRYPTION_KEY) |
APP_URL, SHORT_DOMAIN | Frontend origin and short-link domain |
Feature-specific (optional)
| Variable | Enables |
|---|---|
SUMIT_COMPANY_ID, SUMIT_API_KEY, SUMIT_API_PUBLIC_KEY, SUMIT_WEBHOOK_SECRET, SUMIT_DOCUMENT_LANGUAGE | Billing (Sumit) |
SENDGRID_API_KEY / EMAIL_API_KEY | Verification, invite, reset email |
MAXMIND_LICENSE_KEY | GeoIP city-level analytics |
ANTHROPIC_API_KEY | AI prose (omit → heuristic) |
Other tunables: NODE_ENV, LOG_LEVEL (default info), JWT_EXPIRES_IN (default 7d),
WORKERS_IN_PROCESS, PORT (default 8080), HOST (default 0.0.0.0).
Production on GCP
Deployment is git-driven — pushing to a branch triggers Cloud Build (never run
gcloud builds submit directly). Five Cloud Run (Gen2) services run: api, frontend,
analytics-worker, webhook-worker, and (optionally) docs. In production the API runs
with WORKERS_IN_PROCESS=false so the workers scale independently.
First-time setup:
- Create the GCP project and enable the required APIs.
cd infrastructure/terraform && terraform init && terraform apply.- Populate Secret Manager:
infrastructure/scripts/init-secrets.sh. - Run migrations:
infrastructure/scripts/migrate-db.sh(wrapsprisma migrate deploy). - Connect the GitHub repo to a Cloud Build trigger, or run the GitHub Actions workflows
.github/workflows/cd-staging.ymlandcd-production.ymlmanually. They areworkflow_dispatchonly: LeonAI's own deployment runs on Cloudflare, so these ship as GCP templates rather than live triggers. Wire them tomain→ production anddevelop→ staging yourself if you want them automatic. - Attach custom domains with managed SSL.
- Point the Sumit webhook at
https://api.<domain>/api/v1/billing/sumit-webhook?secret=<SUMIT_WEBHOOK_SECRET>.
Deploy:
# Staging
git push origin develop
# Production (blue-green)
git checkout main && git merge --ff-only develop && git push origin main
Roll back by shifting traffic to a previous revision (migrations are forward-only):
gcloud run services update-traffic <service> --to-revisions <prev-revision>=100
See QUICKSTART.md and docs/DEPLOYMENT-RUNBOOK.md in the repo for the full runbook.