Skip to content

← All guides

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/health healthcheck) 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/jobs with CRON_SECRET; set APP_ENV/VERCEL_ENV correctly 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 run npm run worker in 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

  1. CI green on the release commit (lint, typecheck, unit+integration, build, audit, E2E).
  2. Backup (see §7) and record the migration list: npx prisma migrate status.
  3. Review new migrations for destructive statements (DROP/ALTER TYPE/NOT NULL on populated columns). Destructive changes use expand → migrate data → contract across two releases.
  4. Apply migrations: npx prisma migrate deploy (with DIRECT_URL = migrator role; new tables need RLS — the policy-coverage test in CI enforces it) — as a pre-deploy job, never migrate dev/reset in production, never automatically on app start.
  5. Deploy new image (rolling). Wait for /api/ready = 200.
  6. Run the smoke test (§9). If it fails → rollback (§8).
  7. Seed default plans only: npm run db:seed (idempotent; never db: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 → run npx prisma migrate status against it → boot app against it → run smoke test → verify ledger integrity: SELECT sum(debit)-sum(credit) FROM "JournalLine"; must be 0 per tenant and StockBalance.quantity must equal sum(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/health 200; GET /api/ready 200
  • 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.

Proventory documentation