# Self-hosting & running the SaaS

Source: https://developers.leonai.io/docs/self-hosting/

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

```bash
# 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](https://developers.leonai.io/docs/guides/two-factor/) enrollment (else `503`)                                       |
| `DNS_ENCRYPTION_KEY`                       | Encrypts stored Cloudflare tokens for [domain auto-connect](https://developers.leonai.io/docs/guides/domains/) (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)](https://developers.leonai.io/docs/guides/billing/) |
| `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:

1. Create the GCP project and enable the required APIs.
2. `cd infrastructure/terraform && terraform init && terraform apply`.
3. Populate Secret Manager: `infrastructure/scripts/init-secrets.sh`.
4. Run migrations: `infrastructure/scripts/migrate-db.sh` (wraps `prisma migrate deploy`).
5. Connect the GitHub repo to a Cloud Build trigger, or run the GitHub Actions workflows
   `.github/workflows/cd-staging.yml` and `cd-production.yml` manually. They are
   **`workflow_dispatch` only**: LeonAI's own deployment runs on Cloudflare, so these
   ship as GCP templates rather than live triggers. Wire them to `main` → production
   and `develop` → staging yourself if you want them automatic.
6. Attach custom domains with managed SSL.
7. Point the Sumit webhook at `https://api.<domain>/api/v1/billing/sumit-webhook?secret=<SUMIT_WEBHOOK_SECRET>`.

Deploy:

```bash
# 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):

```bash
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.
