Infrastructure & health
Environments, health endpoints, observability, scheduled jobs, and secrets hygiene.
Environments
The platform runs two separate environments, each from its own branch and its own database branch.
| Environment | Branch | Application URL | Database | |
|---|---|---|---|---|
| Production | main | https://nia-lp-saas.vercel.app | Neon production | Real email |
| UAT | develop | https://nia-lp-saas-uat.vercel.app | Neon uat | Console only (no sending) |
Merging a pull request into develop deploys UAT. Merging develop → main deploys production.
These are genuinely separate. A change verified on UAT is not live for clients until the
develop → main release merge happens.
The other applications
| Application | URL | Repo path |
|---|---|---|
| Marketing | https://nia-lp-marketing.vercel.app | apps/marketing |
| Docs | https://nia-lp-docs.vercel.app | apps/docs |
Reachability of the platform, docs and marketing sites is checked by the production smoke script
(tooling/scripts/src/prod-smoke.ts).
Infrastructure at a glance
| Component | Detail |
|---|---|
| Database | Neon Postgres, EU region, with pgvector and HNSW indices |
| Storage | Cloudflare R2 — avatars, templates, submissions, reports, policies |
| AI | OpenRouter — embeddings plus chat completions, routed per task |
| Document parsing | External parsing service for Word, PDF and image files |
| Resend | |
| Function region | London, for low latency to the EU database |
Database migrations
Migrations run automatically before every deployment build
(apps/saas/scripts/migrate-on-deploy.mjs in the Vercel build command), so a schema change cannot
reach an environment after the code that depends on it.
The migrate step fails the build if the build environment has no DATABASE_URL. It used to
skip silently, which let a deployment look successful while the schema never moved — that is the
failure this now refuses. Preview builds are exempt and still skip. When setting up an
environment, confirm it carries DATABASE_URL before the first deploy, or that deploy will fail
rather than quietly ship against an unmigrated schema.
Health endpoints
| Endpoint | Response | Use for |
|---|---|---|
GET /api/health | 200 OK text | Simple liveness check |
GET /api/health/live | {"status":"up","checkedAt":"..."} | JSON liveness |
GET /api/health/ready | 200 if ready, 503 if not | Load-balancer and uptime monitoring |
GET /api/health/sentry-test | Intentional error | Sentry integration testing (non-prod only) |
The readiness endpoint probes Postgres connectivity, the pgvector extension, the ingestion job
table, the object-storage endpoint, and whether the AI gateway key is configured. If any probe fails
it returns 503 with a body naming the component that is down.
There is still no external uptime monitor watching these endpoints — that stays a deliberately parked decision. What does exist below: a daily check for cron-heartbeat staleness and email-delivery failure spikes that emails the operator once per new incident. It does not cover the platform going down out of hours generally — someone still has to look for that.
Alerts & email health
| Page | What it covers |
|---|---|
Alerts (/operator/alerts) | Cron-heartbeat staleness and email-failure-spike incidents, with state per incident |
Email health (/operator/email) | Sending-domain verification status and a 7-day failure count with the recent failures list |
Observability
- Request tracing: every API call carries an
X-Request-IDheader for log correlation. - Errors: captured to Sentry when
SENTRY_DSNis set. Set bothSENTRY_DSN(server) andNEXT_PUBLIC_SENTRY_DSN(client) for full coverage. - Logs: structured logs via
@repo/logs, viewable in the Vercel dashboard under the deployment's logs.
Scheduled jobs
| Job | Schedule | What it does |
|---|---|---|
/api/cron/process-ingestion | Daily, 03:00 | Drains the document ingestion queue |
/api/cron/purge-conversations | Daily, 04:00 | Purges conversations past their retention window |
/api/cron/kpi-mapping | Daily, 05:00 | Runs scheduled KPI matching |
Cron endpoints are protected by a shared secret header.
Backups
The database is copied to storage held away from the database provider by a job configured to run nightly; that job is not running on a schedule at present (copies exist for 1, 2 and 3 September 2026), and the provider's 6-hour point-in-time recovery window is the control for database restores. File backups are deleted 35 days after they are taken.
Live client data
Changes to live client data are only made through a written runbook with explicit, per-run human approval. Nothing is automated against production data.
Secrets hygiene
All secrets are Vercel environment variables and must never be committed.
| Secret | Notes |
|---|---|
DATABASE_URL | Neon connection string — strictly confidential |
BETTER_AUTH_SECRET | Session signing key — rotating it invalidates all active sessions |
OPENROUTER_API_KEY | High quota cost; revoke immediately if exposed |
RESEND_API_KEY | Revoke and regenerate if exposed |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | R2 credentials — restrict to required buckets |
CRON_SECRET | Shared secret for the cron endpoints |
Rotation cadence
| Secret | Cadence |
|---|---|
CRON_SECRET | Every 90 days |
OPENROUTER_API_KEY, RESEND_API_KEY | Every 6 months or on exposure |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | Every 6 months or on exposure |
BETTER_AUTH_SECRET | Only if compromised |
DATABASE_URL password | On exposure or annual audit |
Practices
- Never commit secrets. Audit the environment variable list periodically and remove unused keys.
- Watch the AI gateway and email provider usage dashboards for anomalous spikes, which can indicate a stolen key.
- Keep secret values out of log output.
- Sign-in and sign-up rate limiting is provided by the auth layer; watch logs for brute-force patterns.
Daily check
- Hit
/api/health/readyon production and confirm every component is green. - Review Usage & cost for spend anomalies.
- Check Ingestion Health for failed jobs and retry or investigate.
- Check Alerts for any open cron-staleness or email-failure incident.
- Confirm the latest intended commit is actually deployed to the environment you think it is.