# Deployment notes

## Environment variables

| Variable | Purpose |
| --- | --- |
| `DATABASE_URL` | MariaDB/MySQL connection string |
| `AUTH_SECRET` | ≥32 random bytes (`openssl rand -base64 32`); also keys signed tokens and IP hashes. Rotating it invalidates sessions, preview links and form tokens |
| `AUTH_URL` | Public https origin of the admin app. Its host is treated as an admin host |
| `AUTH_TRUST_HOST` | `true` behind a reverse proxy |
| `ADMIN_HOSTS` | Comma-separated extra admin hostnames. Every other hostname is served as a customer website |
| `TRUST_PROXY` | `true` ONLY if a trusted proxy sets `X-Forwarded-For` (the right-most entry is used). Otherwise client IPs are not recorded and per-IP limits are skipped |
| `CRON_SECRET` | ≥16 chars. Enables `/api/cron/*`; requests need `Authorization: Bearer <secret>` |
| `MAIL_TRANSPORT` | `console` (default, logs counts only) or `none`. No SMTP transport exists yet |
| `STORAGE_DIR`, `MAX_SITE_STORAGE_MB` | Media directory (must persist) and per-site quota |
| `SEED_*` | Seed only; never set in production. The demo seed refuses to run when `NODE_ENV=production` |

## Scheduled jobs (call from your scheduler, e.g. every minute)

```bash
curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" https://admin.example.com/api/cron/publish   # scheduled publish/unpublish
curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" https://admin.example.com/api/cron/jobs      # notification jobs + retention purge
curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" https://admin.example.com/api/cron/reports   # scheduled reports (hourly)
curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" https://admin.example.com/api/cron/uptime    # uptime checks (every 5-15 minutes)
```

## Release steps

1. `pnpm install --frozen-lockfile`
2. `pnpm db:deploy` (committed migrations only; never `migrate dev` or `reset` in production)
3. `pnpm db:seed` once with `SEED_DEMO_DATA` unset to create permissions, roles and the first super admin; then remove the password variable
4. `pnpm build && pnpm start`

## Safe schema changes (read before running any Prisma command)

This project may have been created with `prisma db push`, so there is **no migration history yet**. Whenever Prisma asks to **reset** the database, answer **no** and stop.

1. Back up first: `mysqldump --single-transaction --routines showoff_cms > backup.sql`.
2. Preview without touching data (flags differ between Prisma versions; confirm with `pnpm prisma migrate diff --help`): `pnpm prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script`. Only additive statements (CREATE TABLE, ADD COLUMN with NULL/default) are safe to apply as is.
3. Apply additive changes: `pnpm prisma db push` (it refuses data-losing changes unless you pass `--accept-data-loss`; never pass that or `--force-reset`).
4. To start a migration history for an existing database: create `prisma/migrations/0_init/migration.sql` with `pnpm prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script`, then mark it applied without running it: `pnpm prisma migrate resolve --applied 0_init`. From then on use `migrate dev` locally and `migrate deploy` in production.

## Before going live

- HTTPS only (Auth.js then issues `__Secure-` cookies; HSTS is sent in production).
- Point each customer domain at the app; add it as a Domain row (Super Admin → Organization → Add a website). Ownership of the domain is not verified by the platform.
- Rate limiters are per process. With several instances, add an edge/Redis limiter.
- Back up the database and the media directory before every release.
- Run the integration tests only against a disposable test database: `TEST_DATABASE_URL=... pnpm test`.

## Domains: going live and working locally

The platform picks the website from the **hostname of each request**, matched against the site's domains (Super Admin → Organizations → the organization → Add a website, which stores the primary domain). Any hostname that is not an admin host (`ADMIN_HOSTS`, the `AUTH_URL` host, `localhost`) is served as a customer website; an unknown hostname returns 404.

**Live**
1. Add the website with its real domain (for example `www.client.com`).
2. Point DNS (A/AAAA or CNAME) for that name at the server running this app, and terminate HTTPS in front of it (reverse proxy or CDN) forwarding the original `Host` header.
3. If a proxy sets `X-Forwarded-For`, set `TRUST_PROXY=true`. Never set it when the app is directly reachable.
4. The admin application must live on a different hostname (`AUTH_URL`), never on a customer domain. The platform does not verify domain ownership; only add domains you control.

**Local development**
1. Add the website with a name such as `client.local`.
2. Add a hosts-file line pointing it at your machine: `127.0.0.1 client.local`
   - Windows: edit `C:\Windows\System32\drivers\etc\hosts` as Administrator.
   - macOS/Linux: `/etc/hosts` (needs sudo).
3. Open `http://client.local:3000` (match your dev port). The admin stays on `http://localhost:3000`.
4. Each additional website needs its own hosts line. Browsers may cache DNS; restart the browser if a new name does not resolve.


## Upgrading an existing database for the new editor and Panels

The page-editor rebuild adds columns to `Page` (`showTitle`, `layout`, `sortOrder`, `showInSearch`, `tags`) and a new `Panel` table. Nothing is dropped or rewritten, and existing page content keeps working.

1. Take a database backup.
2. Run `prisma/manual/2026-10-editor-sections-panels.sql` once against the application database (it is safe to re-run).
3. Deploy the new build and restart the app.

On a brand-new database, `pnpm prisma migrate deploy` / `db push` creates these from `prisma/schema.prisma` as usual.
