Skip to content

Deployment

Who this is for: Platform operators and DevOps
What you'll achieve: Host SchoolRota in a UK-friendly environment with migrations and docs

Hosting guidance

Not for shared cPanel. Prefer:

  • Fly.io / Railway / small UK VPS
  • Azure if a trust requires it

Target region: UK-first (eu-west-2 or equivalent) for school data residency.

Application runtime

ProcessPurpose
Next.js web (apps/web)Tenant portals, operator UI, marketing, Stripe, tRPC, SSE
Optional worker (apps/worker)BullMQ background jobs when Redis is available

Local default database is SQLite. Production staging on Fly uses SQLite on a volume (single machine). Postgres for scaled production — see repository docs/BILLING_AND_HOSTING.md.

Environment variables

Copy from .env.example and set production values:

VariableNotes
DATABASE_URLSQLite file or Postgres URL
OPERATOR_API_TOKENRequired for /operator in production
OPERATOR_AUTH_DISABLED1 for local dev only
SESSION_SECRETLong random string
APP_URLPublic base URL
EMAIL_FROMFrom header
EMAIL_PROVIDERconsole or resend
RESEND_API_KEYWhen using Resend
STRIPE_*Self-serve signup (see BILLING_AND_HOSTING.md)
REDIS_URLOptional acceleration; durable calendar jobs still require cron recovery
INTEGRATION_ENCRYPTION_KEYRequired for Outlook (and future) integration secrets — min 16 characters
CRON_SECRETRequired for /api/cron/calendar-sync (fail-closed without it)

Outlook calendar sync

When enabling Outlook calendar for pilot schools:

  1. Set INTEGRATION_ENCRYPTION_KEY and CRON_SECRET in production secrets.
  2. Schedule POST /api/cron/calendar-sync with Authorization: Bearer {CRON_SECRET} (required for correctness). Redis + apps/worker accelerates jobs but does not replace cron recovery.
  3. Operator enables outlook_calendar for the tenant.
  4. School admin completes Entra app registration and Admin integration setup.

Pending sync jobs recover automatically when the worker or cron endpoint runs (even if Redis was down at publish time).

  1. fly volumes create schoolrota_data --region lhr --size 1
  2. fly secrets set OPERATOR_API_TOKEN=... APP_URL=https://... SESSION_SECRET=...
  3. fly deploy
  4. SSH: run migrations against /data/schoolrota.sqlite
  5. Create first tenant via /operator or CLI
  6. Smoke-test login → setup → publish → export

Migrating from Dutylead (pre-rebrand)

If you already run the old dutylead Fly app:

  1. Fly app — create fly apps create schoolrota (or rename the existing app) and update fly.toml / CI deploy target.
  2. Volume / database — on the mounted volume, rename the SQLite file if needed:
    bash
    flyctl ssh console -a schoolrota -C "mv /data/dutylead.sqlite /data/schoolrota.sqlite"
    Or keep DATABASE_URL=file:/data/dutylead.sqlite in Fly secrets until you cut over.
  3. DNS — point schoolrota.com, *.schoolrota.com, and docs.schoolrota.com at Fly (or your host).
  4. Secrets — set APP_URL=https://schoolrota.com, EMAIL_FROM=SchoolRota <noreply@schoolrota.com>, and update Stripe webhook URL to https://schoolrota.com/api/billing/webhook.
  5. IdP / SCIM — new schools should use SchoolRota-* group names; legacy Dutylead-* groups still map correctly.
  6. Outlook — existing synced events keep working (legacy extension id supported); new syncs use com.schoolrota.calendar.

Deploy checklist (generic)

  1. Set secrets and APP_URL
  2. Run migrations: pnpm db:migrate
  3. Seed only if creating the demo/bootstrap data is intentional
  4. Start web (and worker if Redis is configured)
  5. Provision the first real tenant via /operator or CLI
  6. Smoke-test login → setup → build day → publish → export
  7. Confirm email delivery path is not console in production

Documentation site

This VitePress site builds to static files:

bash
pnpm docs:build

Output: docs-site/.vitepress/dist

Deploy options:

  • GitHub Pages — see .github/workflows/docs.yml
  • Cloudflare Pages / Netlify — point at the same build command and output directory
  • Custom domain such as docs.schoolrota.com

Preview locally:

bash
pnpm docs:dev
# or after build:
pnpm docs:preview

SchoolRota documentation — every slot covered, every day.