Prerequisites
Before deploying, ensure the following are in place:
- Vercel account with a Team Plan (Pro or Enterprise) — required for environment variable scoping, deployment protection, and team audit logs
- GitHub repository connected to the Vercel team
- Neon PostgreSQL database provisioned (separate databases for staging and production)
- Cloudflare account with
pensionportal.ai domain configured
Initial Vercel Setup
Follow these steps to configure the Vercel project from scratch:
- Connect GitHub repository — In the Vercel Dashboard, click “Add New Project” and import the
IORP-II-App repository from GitHub.
- Set Framework Preset — Vercel auto-detects Next.js. Confirm the preset is set to Next.js.
- Set Build Command —
npm run build
- Set Output Directory —
.next (auto-configured by Vercel for Next.js)
- Set Install Command —
npm install
- Set Node.js Version — Select 20.x in Project Settings → General → Node.js Version.
Domain Mapping
| Deployment | Domain | Notes |
|---|
| Production | app.pensionportal.ai | Custom domain added in Vercel Dashboard |
| Marketing | www.pensionportal.ai | Separate Vercel project or same project with multiple domains |
| PR Previews | *.vercel.app (auto-generated) | No custom domain needed for preview deployments |
To add the custom domain:
- Go to Vercel Dashboard → Your Project → Settings → Domains
- Add
app.pensionportal.ai
- Vercel will display a CNAME record to add in Cloudflare DNS
- In Cloudflare, add the CNAME pointing to
cname.vercel-dns.com with Proxy status: Proxied
See the Cloudflare Configuration Guide for the full DNS record table.
Environment Variable Matrix
Configure environment variables in Vercel Dashboard → Project → Settings → Environment Variables. Set each variable for the correct environment scope (Development, Preview, Production).
| Variable | Development | Staging (Preview) | Production |
|---|
DATABASE_URL | postgresql://localhost:5432/iorp_dev | postgresql://neon-staging.../iorp_staging | postgresql://neon-prod.../iorp_prod |
DATABASE_URL_UNPOOLED | Same as DATABASE_URL | Neon staging unpooled URL | Neon prod unpooled URL |
AUTH_SECRET | Any 32+ character string | Generated secret (Vercel encrypted) | Generated secret (Vercel encrypted, different from staging) |
AUTH_URL | http://localhost:3000 | https://iorp-staging.vercel.app | https://app.pensionportal.ai |
ANTHROPIC_API_KEY | sk-ant-... (dev key) | sk-ant-... (staging key) | sk-ant-... (prod key, rate-limited) |
NEXT_PUBLIC_APP_URL | http://localhost:3000 | https://iorp-staging.vercel.app | https://app.pensionportal.ai |
PPS_ENCRYPTION_KEY | 64-char hex (dev only, never real data) | Staging hex key (Vercel encrypted) | Prod hex key (Vercel encrypted, NEVER same as staging) |
RESEND_API_KEY | Optional | re_staging_... | re_prod_... |
ADMIN_EMAIL | dev@local | staging-admin@pensionportal.ai | admin@pensionportal.ai |
EMAIL_FROM | noreply@local | noreply@staging.pensionportal.ai | noreply@pensionportal.ai |
NODE_ENV | development (auto) | production (auto) | production (auto) |
PPS_ENCRYPTION_KEY must be unique per environment. A staging key used in production would mean that data encrypted in staging could be decrypted in production and vice versa — a critical GDPR and IORP II violation. Rotate immediately if a key is ever shared across environments.
AUTH_SECRET must be unique per environment. Sharing secrets across environments allows session tokens from one environment to be valid in another.
Production secrets must never appear in .env files committed to the repository. All sensitive values must be set exclusively through the Vercel Dashboard with appropriate environment scoping.
Generating Secrets
Generate AUTH_SECRET:
Generate PPS_ENCRYPTION_KEY:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
CI Enforcement on Vercel
Vercel automatically runs the following checks during each deployment build. All must pass before the deployment is promoted:
npm run lint — ESLint must report zero errors
- TypeScript compilation — Run as part of the Next.js build step; type errors fail the build
npm run build — Full Next.js production build must complete without errors
Recommended GitHub Actions Integration
Add the following required status checks in GitHub Branch Protection Rules so that Vercel deployments are gated on CI results:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, staging]
pull_request:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm test
docs-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run docs:check
Configure these jobs as required status checks in GitHub → Repository → Settings → Branches → Branch protection rules for main.
Rollback Procedure
Use this procedure when a production deployment introduces a regression:
- Go to Vercel Dashboard → Your Project → Deployments
- Identify the last known-good deployment (check timestamps and commit messages)
- Click the ”…” menu next to that deployment → Promote to Production
- Verify
https://app.pensionportal.ai is serving the correct version (check the deployment SHA in response headers or the health endpoint)
- If a database migration was included in the bad deployment: run the rollback migration against production:
DATABASE_URL=<prod-url> npm run db:migrate:rollback
- Alert the team in Slack
#ops with:
- Incident start time
- Affected deployment SHA
- Rollback deployment SHA
- Any data impact assessment
If the rollback involves a destructive schema change (e.g., column drop), coordinate with the engineering lead before running db:migrate:rollback. Irreversible migrations may require a point-in-time restore from Neon. See the Backup & Restore Runbook.
Preview Deployments
Vercel automatically creates a preview deployment for every pull request.
- Preview URLs follow the pattern:
https://iorp-ii-app-<hash>.vercel.app
- Preview deployments use Preview scoped environment variables (staging values)
- Share preview URLs with stakeholders for review before merging
Do NOT set PPS_ENCRYPTION_KEY to the production value in Preview environment variables. Preview deployments should use a dedicated preview/staging key. Real beneficiary PPS data must never be present in preview databases.
Database migrations must not run automatically on preview deployments. The npm run db:migrate command should only be executed manually against staging or production URLs, never as part of the Vercel build step.
Production Health Checks
The application exposes the following health endpoints. Configure an external uptime monitor (e.g., BetterUptime, UptimeRobot, or Vercel Monitoring) to poll these on a schedule.
| Endpoint | Expected Response | Check Interval |
|---|
GET https://app.pensionportal.ai/api/health/live | 200 {"status":"ok"} | Every 60 seconds |
GET https://app.pensionportal.ai/api/health/ready | 200 {"status":"ready"} | Every 60 seconds |
GET https://app.pensionportal.ai/api/health/db | 200 {"status":"ok","latencyMs":N} | Every 60 seconds |
Example: BetterUptime monitor configuration
URL: https://app.pensionportal.ai/api/health/live
Method: GET
Check interval: 60 seconds
Alert threshold: 2 consecutive failures
Alert channels: PagerDuty, Slack #ops
If latencyMs on the /api/health/db endpoint consistently exceeds 500ms, investigate Neon connection pooling configuration and Vercel region alignment.
Database Migration Process
Schema changes must be applied to staging before production. Never run migrations directly against production without staging validation.
- Run migration against staging first:
DATABASE_URL=<staging-url> npm run db:migrate
-
Validate staging — smoke test key flows (login, scheme creation, compliance check) against the staging URL.
-
For significant schema changes, consider maintenance mode — place production in a read-only or maintenance state to prevent writes during migration. For additive-only migrations (new columns with defaults, new tables), maintenance mode is optional.
-
Run migration against production:
DATABASE_URL=<prod-url> npm run db:migrate
- Deploy the new application code via Vercel (merge to
main or trigger a manual promotion).
Neon supports branching — consider creating a Neon branch from production to test migrations against a copy of real schema before applying to production. See Neon Branching for details.