PRODUCTION DEPLOYMENT
Status: the Vercel + Neon path is in use. The Docker image build and Docker Compose setup have not been run on real infrastructure yet; treat them as unverified until you run the smoke tests in §9. Payment providers and email delivery need a live test with your own accounts before you rely on them.
1. Recommended topology
- App: container from the
Dockerfile(Next.js standalone, non-root,/api/healthhealthcheck) on a container platform (Fly.io, Render, Railway, AWS ECS/Fargate, Azure Container Apps…), ≥2 instances behind HTTPS load balancer. - Vercel: see docs/GUIDE_LOCAL_TEST_AND_VERCEL.md (no worker; Vercel Cron calls
/api/cron/jobswithCRON_SECRET; setAPP_ENV/VERCEL_ENVcorrectly so the sandbox payment provider is refused in production). - Worker: same image, command
npx tsx src/worker/index.ts(needs the full repo/deps — build a worker target or runnpm run workerin a second service), 1 instance. - Database: managed PostgreSQL 15/16 (RDS, Neon, Supabase, Azure Flexible Server, Render PG) with automated backups + PITR, TLS required, private networking.
- Object storage: not required yet (no uploads implemented).
- Email: a transactional provider (SES, Postmark, Resend). A Resend adapter exists (
EMAIL_PROVIDER=resend) but is unverified against a live account; verify your sending domain and test delivery. Without it, sign-up still works but verification/reset/invitation emails are not delivered. - Payments: Paystack/Flutterwave. Webhook endpoint:
POST https://<domain>/api/webhooks/billing. Paystack checkout is not implemented; only webhook signature verification is.
2. Environments
| Env | DB | Notes |
|---|---|---|
| dev | local PG bizcore |
npm run dev, console emails, sandbox billing, db:seed:demo allowed |
| test | bizcore_test |
wiped by tests/E2E; CI uses a service container |
| staging | separate managed DB | production build, real email sandbox, provider test keys, no demo seed |
| production | managed DB | provider live keys, backups on, NODE_ENV=production (sandbox billing is refused) |
3. Environment variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL URL for the restricted runtime role bmt_app (NOSUPERUSER, NOBYPASSRLS, owns nothing). If this is a superuser/owner, row-level security is silently bypassed. |
DIRECT_URL |
for migrations | URL of the table-owning migrator role; used only by prisma migrate/db:roles, never by the running app |
APP_URL |
yes | Public base URL (used in emailed links) |
APP_NAME |
no | Product/brand name (default Proventory) |
APP_SECRET |
yes | ≥32 random chars. Encrypts MFA authenticator seeds (AES-256-GCM): keep it stable and backed up in the secret store; rotating it invalidates all enrolled authenticators. The app refuses to enrol/verify MFA if it is missing or short |
PAYMENT_PROVIDER |
yes | paystack in production (sandbox is rejected when NODE_ENV=production) |
PAYSTACK_SECRET_KEY |
if paystack | Secret key; also the webhook HMAC key |
EMAIL_PROVIDER |
yes | resend in production (console/memory are refused or dev-only) |
RESEND_API_KEY, EMAIL_FROM |
if resend | API key and verified sender, e.g. Proventory <no-reply@yourdomain> |
WORKER_INTERVAL_MS |
no | Worker cycle (default 6 h) |
NEXT_OUTPUT |
build | standalone (set by Dockerfile) |
| Never commit values. Use the platform's secret store. |
4. Database provisioning (once)
Two roles are required (row-level security depends on it):
CREATE ROLE bmt_migrator LOGIN PASSWORD '…' CREATEROLE; -- owns the schema; runs migrations; used as DIRECT_URL
CREATE DATABASE bizcore OWNER bmt_migrator;
Then, with DIRECT_URL pointing at bmt_migrator and DATABASE_URL at bmt_app:<password>@…:
npx prisma migrate deploy # creates schema, triggers and RLS policies as the owner
npm run db:roles # creates/refreshes bmt_app (NOSUPERUSER NOBYPASSRLS) and grants DML only
# or, with psql: psql "$DIRECT_URL" -v app_password="'<pw>'" -v dbname=bizcore -f scripts/db-roles.sql
Verify after provisioning (must print f | f):
SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'bmt_app';
Managed providers that only give you a superuser-like default role (some Supabase/Neon defaults) bypass RLS — create the restricted role explicitly and connect the app as it. The app role has UPDATE/DELETE on ledger tables, but append-only triggers block it; consider REVOKE UPDATE, DELETE ON "StockMovement","JournalEntry","JournalLine" FROM bmt_app; (compatible with the code).
5. Release procedure
- CI green on the release commit (lint, typecheck, unit+integration, build, audit, E2E).
- Backup (see §7) and record the migration list:
npx prisma migrate status. - Review new migrations for destructive statements (DROP/ALTER TYPE/NOT NULL on populated columns). Destructive changes use expand → migrate data → contract across two releases.
- Apply migrations:
npx prisma migrate deploy(withDIRECT_URL= migrator role; new tables need RLS — the policy-coverage test in CI enforces it) — as a pre-deploy job, nevermigrate dev/resetin production, never automatically on app start. - Deploy new image (rolling). Wait for
/api/ready= 200. - Run the smoke test (§9). If it fails → rollback (§8).
- Seed default plans only:
npm run db:seed(idempotent; neverdb:seed:demo).
6. Domain, HTTPS
Terminate TLS at the platform/load balancer; redirect HTTP→HTTPS; HSTS is set by the app. Set APP_URL to the final https origin. Cookies are Secure in production.
7. Backups & disaster recovery
- Enable managed automated daily backups + PITR (target RPO ≤ 5 min, RTO ≤ 1 h — confirm with your provider's capabilities).
- Weekly logical dump to separate-account storage:
pg_dump --format=custom --no-owner "$DATABASE_URL" > bizcore-$(date +%F).dump(encrypt at rest). - Restore drill (quarterly): create scratch DB →
pg_restore --no-owner -d scratch bizcore.dump→ runnpx prisma migrate statusagainst it → boot app against it → run smoke test → verify ledger integrity:SELECT sum(debit)-sum(credit) FROM "JournalLine";must be 0 per tenant andStockBalance.quantitymust equalsum(StockMovement.quantity)per (location,batch). - Restore into production only after incident-commander approval; stop the app first; record the data-loss window.
8. Rollback
Application: redeploy previous image tag (images are immutable). Database: migrations are forward-only; rollback = restore PITR to just before the migration only if the migration is destructive/incompatible; otherwise ship a corrective forward migration. Keep the previous app version compatible with the new schema for one release (expand/contract).
9. Production smoke-test checklist
-
GET /api/health200;GET /api/ready200 - Marketing page renders; security headers present (
curl -I) - Sign-up → verification email arrives → verify → onboarding → workspace (use a throwaway business, then suspend it)
- Add product, receive stock, complete a sale, view receipt, check inventory decreased
- Return item; check reports and stock
- Invite a staff member; accept; confirm restricted permissions
- Paystack test-mode webhook with valid and invalid signature → 200 / 401; replay →
duplicate:true - Platform admin login (from a seeded/admin-promoted account), tenant list, plan edit
- Worker log line
"job":"cycle"observed - Backups present and last restore drill date recorded
10. Monitoring & operations
Structured JSON logs from the worker; app logs to stdout (collect with the platform). Wire an error tracker (Sentry etc.) via Next.js instrumentation.ts — not yet wired. Alerts: /api/ready failures, 5xx rate, DB connections/CPU/storage, failed webhook (4xx/5xx), BillingEvent rows with processedAt IS NULL older than 10 min.
11. Secret rotation
DB password: create new role password → update secret store → rolling restart → revoke old. APP_SECRET: do not rotate casually (see above). Provider keys: rotate in provider dashboard, update secret, redeploy (webhook verification uses the same key — rotate during low traffic and expect a brief window of rejected webhooks; providers retry).
12. Incident response (short)
Detect → contain (suspend tenant / revoke sessions / disable provider key) → preserve (AuditLog, DB snapshot) → eradicate → recover (restore if needed) → notify (customers and regulator as legally required — consult counsel; NDPA deadlines apply to personal-data breaches) → post-mortem.