Deployment
Deploy Roxabi on Vercel — both web and API on one platform
Overview
Roxabi uses a staging-based deployment architecture with Vercel as the single platform for both the web app and API.
Feature branch → PR to staging → CI runs
→ (optional) Deploy Preview via GitHub Actions
staging → PR to main → CI runs → Vercel auto-deploys to production| Component | Platform | Details |
|---|---|---|
| Web (TanStack Start + Nitro) | Vercel | SSR, edge caching, Fluid compute |
| API (NestJS + Fastify) | Vercel | Zero-config NestJS, Fluid compute, auto-scaling |
| Docs (Fumadocs + Next.js) | Vercel | Static/ISR — deployed to docs.app.roxabi.com |
| Database | Neon | Serverless PostgreSQL, not managed by Vercel |
No Docker is needed for deployment. Docker configurations (Dockerfiles, docker-compose.prod.yml, Nginx configs, deploy/) remain in the repo for local development only.
Branch Strategy
| Flow | Branch path | Deploys? |
|---|---|---|
| Normal | feat/* → PR to staging → PR to main | Only main merges auto-deploy |
| Hotfix | hotfix/* → PR to main | Auto-deploys immediately |
stagingis the default integration branch — all feature PRs target it- Automatic preview deployments are disabled at the Vercel project level — pushes to non-
mainbranches do not trigger any Vercel build - Use the Deploy Preview GitHub Action or Vercel CLI for on-demand preview URLs
- Only merges to
maintrigger production auto-deploys
Preview Deploys
Preview deploys are manual only, triggered via the GitHub Actions tab.
How to trigger
- Go to Actions → Deploy Preview
- Click Run workflow
- Select the branch (typically
staging) - Choose a target:
web,api,docs, orboth - Click Run workflow
The workflow builds and deploys a Vercel preview. The preview URL appears in the workflow run summary.
When to use
- Before promoting
staging→main, to verify the integration build - To share a preview URL with stakeholders for review
- To smoke-test a specific branch without deploying to production
Custom domain mapping
By default, preview deploys compute APP_URL from the Vercel branch alias (e.g., roxabi-app-git-staging-roxabi.vercel.app). Branches with a custom domain alias (e.g., app.staging.roxabi.com) need APP_URL set to the custom domain so better-auth's CSRF origin check passes.
The workflow handles this automatically for known branches:
| Branch | APP_URL | CORS_ORIGIN |
|---|---|---|
staging | https://app.staging.roxabi.com | https://app.staging.roxabi.com,https://roxabi-app-git-staging-roxabi.vercel.app |
| Other branches | Branch alias | Branch alias |
To add a custom domain for a new branch, add a case entry in the deploy-preview.yml workflow's "Deploy API preview" step.
GitHub Actions Secrets
The Deploy Preview workflow requires these repository secrets:
| Secret | Description | Where to find |
|---|---|---|
VERCEL_TOKEN | Vercel personal access token — used only by the Deploy Preview workflow, not by production CD (which uses the Vercel GitHub Integration) | vercel.com/account/tokens |
VERCEL_ORG_ID | Vercel team/org ID | .vercel/project.json → orgId (after vercel link) |
VERCEL_PROJECT_ID_WEB | Vercel project ID for web | apps/web/.vercel/project.json → projectId |
VERCEL_PROJECT_ID_API | Vercel project ID for API | apps/api/.vercel/project.json → projectId |
VERCEL_PROJECT_ID_DOCS | Vercel project ID for docs | apps/docs/.vercel/project.json → projectId |
NEON_API_KEY | Neon API key (for preview DB branches) | console.neon.tech → Account → API Keys |
NEON_PROJECT_ID | Neon project ID | console.neon.tech → Project Settings |
VERCEL_AUTOMATION_BYPASS_SECRET | API project's Protection Bypass for Automation secret — stored as a Vercel preview env var on the web project so the Nitro server route can read it at runtime and forward it to the SSO-protected API preview | Vercel dashboard → API project → Settings → Deployment Protection → Protection Bypass for Automation → Generate |
Add them at Settings → Secrets and variables → Actions → New repository secret.
Note: The
NEON_API_KEYandNEON_PROJECT_IDsecrets are only required if you use the Deploy Preview workflow to create API preview environments with isolated databases. They are not needed for production-only deployments.
Prerequisites
Install the Vercel CLI globally:
bun add -g vercelAuthenticate:
vercel loginInitial Setup
You need three Vercel projects pointing to the same GitHub repository — one for the web app, one for the API, and one for the docs site.
1. Create Projects
Create all three projects from the Vercel dashboard (one-time setup):
- Sign up at vercel.com and connect your GitHub account
- Click Add New Project, import the repository, set Root Directory to
apps/web - Repeat for
apps/api - Repeat for
apps/docs
Build settings are version-controlled in apps/web/vercel.json and apps/api/vercel.json — no manual configuration needed.
2. Link Projects Locally
Link each app directory to its Vercel project:
cd apps/web && vercel link
cd apps/api && vercel link
cd apps/docs && vercel linkSelect the matching project when prompted. This creates .vercel/project.json in each directory (gitignored).
3. Disable Automatic Preview Deployments
Disable Vercel's automatic preview deployments to prevent rate-limit exhaustion from canceled builds on non-production branches:
# Get your team ID and project IDs from .vercel/project.json after linking
TEAM_ID="<your-team-id>"
TOKEN="<your-vercel-token>"
# Disable for web project
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<web-project-id>?teamId=$TEAM_ID" \
-H "Content-Type: application/json" \
-d '{"previewDeploymentsDisabled":true}'
# Disable for API project
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<api-project-id>?teamId=$TEAM_ID" \
-H "Content-Type: application/json" \
-d '{"previewDeploymentsDisabled":true}'
# Disable for Docs project
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<docs-project-id>?teamId=$TEAM_ID" \
-H "Content-Type: application/json" \
-d '{"previewDeploymentsDisabled":true}'Verify the setting:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<project-id>?teamId=$TEAM_ID" \
| jq '.previewDeploymentsDisabled'
# Should return: trueWhy this matters: Even with
ignoreCommandinvercel.json, Vercel still attempts a deployment for every push, which counts toward the rate limit. ThepreviewDeploymentsDisabledsetting prevents Vercel from initiating the deployment entirely. Use the Deploy Preview GitHub Action or Vercel CLI when you need a preview.
4. Configure Environment Variables
Web project
cd apps/web
for env in production preview development; do
printf '%s' "https://api.roxabi.vercel.app" | vercel env add API_URL "$env"
printf '%s' "https://app.roxabi.vercel.app" | vercel env add APP_URL "$env"
doneWarning: Always use
printf '%s'instead ofechowhen piping values tovercel env add.echoappends a trailing newline that becomes part of the stored value, which causesERR_INVALID_CHARerrors when the values are used in HTTP headers (e.g., CORS origin).
| Variable | Description |
|---|---|
API_URL | Vercel API project URL |
APP_URL | This project's URL |
API project
cd apps/api
# Generate auth secret
SECRET=$(openssl rand -base64 32)
for env in production preview development; do
printf '%s' "https://app.roxabi.vercel.app" | vercel env add CORS_ORIGIN "$env"
printf '%s' "$SECRET" | vercel env add BETTER_AUTH_SECRET "$env"
printf '%s' "https://api.roxabi.vercel.app" | vercel env add BETTER_AUTH_URL "$env"
printf '%s' "https://app.roxabi.vercel.app" | vercel env add APP_URL "$env"
printf '%s' "https://api.roxabi.vercel.app" | vercel env add API_URL "$env"
printf '%s' "noreply@yourdomain.com" | vercel env add EMAIL_FROM "$env"
doneNeon (Database) — Vercel Marketplace Integration
Install the Neon integration to auto-inject DATABASE_URL for production:
- Go to vercel.com/marketplace/neon and click Install
- Select your Vercel team/account and link to the API project
- Verify:
cd apps/api && vercel env ls production | grep DATABASE_URL - Trigger a redeploy:
vercel redeploy <latest-deployment-url> - Test: verify
/api/healthreturns 200
Note: The integration also injects
DATABASE_URLinto preview and development scopes. Preview scope is overridden by the Deploy Preview workflow's--envflag. Development scope is unused (local dev reads.envfiles).
Rollback: If production fails after install, re-add manually:
vercel env add DATABASE_URL productionand redeploy.
Resend (Email) — Vercel Marketplace Integration
Install the Resend integration to auto-inject RESEND_API_KEY:
- Go to vercel.com/marketplace/resend and click Install
- Select your Vercel team/account and link to the API project
- Verify:
cd apps/api && vercel env ls production | grep RESEND - Trigger a redeploy:
vercel redeploy <latest-deployment-url> - Test: trigger a password reset email from the production deployment to confirm the API key works
Rollback: If email fails, uninstall the integration and re-add manually:
vercel env add RESEND_API_KEY production
Upstash Redis (Rate Limiting) — Vercel Marketplace Integration
Rate limiting is enabled by default (RATE_LIMIT_ENABLED=true) and requires Upstash Redis. The code uses @upstash/redis and reads KV_REST_API_URL and KV_REST_API_TOKEN — the env var names auto-injected by the Upstash Marketplace integration.
Step-by-step via Vercel Integration:
- Go to vercel.com/marketplace/upstash and click Install (or go to your team's integrations page:
https://vercel.com/<team>/~/integrations/upstash) - Select your Vercel team/account when prompted
- Choose Redis as the product
- Configure the database:
- Name: e.g.
roxabi-redis - Region: choose the closest to your Neon database region
- Plan: Free tier is sufficient for development
- Name: e.g.
- Link the database to your API project (e.g.
roxabi-api) - Vercel auto-creates
KV_REST_API_URLandKV_REST_API_TOKENas environment variables on the API project - Redeploy the API project for the new env vars to take effect
Via Vercel CLI:
cd apps/api
vercel integration add upstash/upstash-kv --name roxabi-redisNote: The CLI requires accepting the Upstash terms of service in the dashboard first. If you see a "Terms have not been accepted" prompt, visit the integration page above first.
Verify the env vars were added:
cd apps/api && vercel env ls | grep KV_RESTYou should see KV_REST_API_URL and KV_REST_API_TOKEN listed.
Alternative — manual setup (without the integration):
- Create a Redis database at console.upstash.com
- Copy the REST URL and token from the database details page
- Add them to the API project:
cd apps/api
for env in production preview development; do
printf '%s' "https://your-redis.upstash.io" | vercel env add KV_REST_API_URL "$env"
printf '%s' "your-upstash-token" | vercel env add KV_REST_API_TOKEN "$env"
doneWarning: Without these vars, the API will crash at startup in production. If you do not need rate limiting (e.g., early development), set
RATE_LIMIT_ENABLED=falseexplicitly — but note this disables auth brute-force protection.
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string (auto-injected by Neon Marketplace integration) |
DATABASE_APP_URL | App user connection URL with RLS enforced (use instead of DATABASE_URL for API server queries) |
CORS_ORIGIN | Web project URL (for CORS) |
BETTER_AUTH_SECRET | Random 32+ character string |
BETTER_AUTH_URL | This project's URL |
APP_URL | Web project URL |
API_URL | This project's URL |
EMAIL_FROM | Sender email address |
RESEND_API_KEY | Resend API key for transactional emails (auto-injected by Resend Marketplace integration) |
KV_REST_API_URL | Upstash Redis REST URL (auto-injected by Upstash Marketplace integration) |
KV_REST_API_TOKEN | Upstash Redis REST token (auto-injected by Upstash Marketplace integration) |
Credential Rotation
| Credential | Rotation method |
|---|---|
DATABASE_URL (production) | Auto-managed via Neon Marketplace integration — no manual action needed |
RESEND_API_KEY (production) | Auto-managed via Resend Marketplace integration — no manual action needed |
KV_REST_API_URL / KV_REST_API_TOKEN (production) | Auto-managed via Upstash Marketplace integration — no manual action needed |
NEON_API_KEY (GitHub Actions) | Manual rotation: GitHub Settings → Secrets → update NEON_API_KEY |
NEON_PROJECT_ID (GitHub Actions) | Static — only changes if Neon project is recreated |
BETTER_AUTH_SECRET | Manual rotation: vercel env rm BETTER_AUTH_SECRET production && vercel env add BETTER_AUTH_SECRET production |
Tip:
BETTER_AUTH_SECRETrotation requires a redeploy to take effect. Betweenenv rmand the completed redeploy with the new value, auth sessions will be invalid. Plan rotation during a maintenance window.
5. Verify
cd apps/web && vercel env ls
cd apps/api && vercel env lsFork Setup Checklist
If you are forking the Roxabi Boilerplate to start your own SaaS project, follow this step-by-step checklist to get a fully working CI/CD pipeline.
1. Create Vercel Projects
Create three Vercel projects from the same GitHub repository:
- Sign up at vercel.com and connect your GitHub account
- Click Add New Project, import the repository, set Root Directory to
apps/web - Repeat for a second project with Root Directory set to
apps/api - Repeat for a third project with Root Directory set to
apps/docs - Link each project locally with
vercel link(see Initial Setup above)
2. Create and Initialize the Neon Database
- Create a Neon account and project
Tip: Instead of manual setup, you can install the Neon Marketplace integration to auto-inject
DATABASE_URLinto your Vercel API project. You'll still need to initialize the database schema (steps 3-4 below).
- Copy the
DATABASE_URLconnection string from the Neon dashboard - Initialize the database schema (required before any migration can run — see Database Initialization for why):
cd apps/api
DATABASE_URL="<your-neon-connection-string>" bunx drizzle-kit push --force- Register existing migrations so Drizzle does not re-apply them:
cd apps/api
DATABASE_URL="<your-neon-connection-string>" bun run db:migrate3. Configure GitHub Actions Secrets
Add these 8 secrets at Settings > Secrets and variables > Actions > New repository secret:
| Secret | Description | Where to find |
|---|---|---|
VERCEL_TOKEN | Vercel personal access token — used only by the Deploy Preview workflow, not by production CD (which uses the Vercel GitHub Integration) | vercel.com/account/tokens |
VERCEL_ORG_ID | Vercel team/org ID | .vercel/project.json > orgId (after vercel link) |
VERCEL_PROJECT_ID_WEB | Vercel project ID for web | apps/web/.vercel/project.json > projectId |
VERCEL_PROJECT_ID_API | Vercel project ID for API | apps/api/.vercel/project.json > projectId |
VERCEL_PROJECT_ID_DOCS | Vercel project ID for docs | apps/docs/.vercel/project.json > projectId |
NEON_API_KEY | Neon API key (for preview DB branches) | console.neon.tech > Account > API Keys |
NEON_PROJECT_ID | Neon project ID | console.neon.tech > Project Settings |
VERCEL_AUTOMATION_BYPASS_SECRET | API project's Protection Bypass for Automation secret — stored as a Vercel preview env var on the web project so the Nitro server route can read it at runtime and forward it to the SSO-protected API preview | Vercel dashboard > API project > Settings > Deployment Protection > Protection Bypass for Automation > Generate |
4. Provision Upstash Redis (Rate Limiting)
Rate limiting is enabled by default and requires Upstash Redis. Follow the step-by-step in Upstash Redis setup above, or set RATE_LIMIT_ENABLED=false temporarily if you need to deploy without it.
Note: If you skip this step, the API will crash at startup in production.
5. Install Resend Integration (Email)
Install the Resend integration for transactional emails:
- Go to vercel.com/marketplace/resend and click Install
- Link to the API project
- Verify:
cd apps/api && vercel env ls production | grep RESEND - Redeploy:
vercel redeploy <latest-deployment-url>
Alternative: Add manually:
vercel env add RESEND_API_KEY production
6. Disable Automatic Preview Deployments
Disable Vercel's automatic preview deployments on all three projects to prevent rate-limit exhaustion. See Disable Automatic Preview Deployments for the curl commands.
7. Deploy to Production
Push to main (or merge staging into main) to trigger the initial production deploy:
git push origin mainVercel auto-deploys both projects. Verify by checking the deployment URLs in the Vercel dashboard.
8. (Optional) Configure Deployment Protection
Both Vercel projects ship with SSO deployment protection enabled by default. See Deployment Protection for options to adjust this for preview environments.
Daily Operations
Check Deployment Status
cd apps/web && vercel ls # Web deployments
cd apps/api && vercel ls # API deploymentsView Logs
vercel logs <deployment-url>Inspect a Deployment
vercel inspect <deployment-url>
vercel inspect <deployment-url> --logs # With build logsRollback
vercel promote <previous-deployment-url>Or find the URL first:
vercel ls # Find the working deployment
vercel promote <url> # Promote it to productionRedeploy
vercel redeploy <deployment-url>Manage Environment Variables
vercel env ls # List all
vercel env add SECRET_NAME production # Add (interactive)
vercel env rm SECRET_NAME production # Remove
vercel env pull .env.local # Pull to local fileBuild Settings
Build settings are version-controlled in vercel.json files — no dashboard configuration needed.
apps/web/vercel.json:
{
"ignoreCommand": "[ \"$VERCEL_GIT_COMMIT_REF\" != \"main\" ] || npx turbo-ignore @repo/web",
"installCommand": "bun install --ignore-scripts",
"buildCommand": "turbo run build"
}apps/api/vercel.json:
{
"ignoreCommand": "[ \"$VERCEL_GIT_COMMIT_REF\" != \"main\" ] || npx turbo-ignore @repo/api",
"installCommand": "bun install --ignore-scripts",
"buildCommand": "bun run db:migrate && turbo run build"
}apps/docs/vercel.json:
{
"ignoreCommand": "[ \"$VERCEL_GIT_COMMIT_REF\" != \"main\" ] || npx turbo-ignore @repo/docs",
"installCommand": "bun install --ignore-scripts",
"buildCommand": "bun run codegen && next build"
}
Nitro (TanStack Start) outputs to `.vercel/output/` using the Vercel Build Output API — no `outputDirectory` override needed.
> **Important:** Dashboard build settings (Install Command, Build Command) must match `vercel.json` values. If they diverge, Vercel shows a "Configuration Settings differ" warning. Use the Vercel API to sync them if needed:
> ```bash
> curl -X PATCH -H "Authorization: Bearer $TOKEN" \
> "https://api.vercel.com/v9/projects/$PROJECT_ID?teamId=$TEAM_ID" \
> -H "Content-Type: application/json" \
> -d '{"buildCommand":"bun run db:migrate && turbo run build","installCommand":"bun install --ignore-scripts"}'
> ```
---
## Build-Only on Main (Avoid Duplicate Deploys)
Automatic preview deployments are **disabled at the Vercel project level** via the `previewDeploymentsDisabled` API setting. This prevents Vercel from attempting (and canceling) preview builds on every push to non-production branches, which would otherwise consume the deployment rate limit.
As a secondary safeguard, both projects also use `ignoreCommand` in `vercel.json` to skip builds on non-`main` branches:
```json
"ignoreCommand": "[ \"$VERCEL_GIT_COMMIT_REF\" != \"main\" ] || npx turbo-ignore @repo/web"| Layer | Mechanism | Purpose |
|---|---|---|
| Primary | previewDeploymentsDisabled: true (project setting) | Prevents Vercel from even attempting preview deployments |
| Secondary | ignoreCommand in vercel.json | Skips builds on non-main branches if a deployment is triggered |
Preview deploys are manual only — use the Deploy Preview GitHub Action (see Preview Deploys above) or the Vercel CLI.
Important: Always run
vercelpreview deploys from the repository root, not fromapps/weborapps/api. The Vercel project linking (.vercel/project.json) and TurboRepo build pipeline expect the root as the working directory. Running from a subdirectory may produce broken builds or skip workspace dependencies.
vercel # Preview deploy (from repo root)Database Migrations
Database migrations run automatically during the Vercel build and are protected by CI schema drift detection.
Automatic Migrations
The API build command runs migrations before building:
"buildCommand": "bun run db:migrate && turbo run build"| Behavior | Details |
|---|---|
| When | Every API deploy to production (merge to main) |
| Idempotent | Drizzle Kit tracks applied migrations — re-running is safe |
| On failure | Build fails, Vercel keeps the previous production deployment live |
| On success | Build continues, new code deploys with the updated schema |
Note: Because migrations run before the build, your database schema is always at least as new as the deployed code. There is no window where new code runs against an old schema.
Manual Rollback
Drizzle Kit does not support automatic rollback (db:rolldown). If a migration causes issues:
| Strategy | Steps |
|---|---|
| Corrective migration (preferred) | Write a new migration that undoes the change, commit, and push. The next deploy applies it automatically. |
| Revert and redeploy | Revert the commit containing the bad migration, push to main. Vercel redeploys with the previous schema. Only works if the migration was additive (e.g., adding a column). Destructive migrations (dropping columns/tables) cannot be reverted this way. |
Schema Drift Detection
CI automatically detects when a developer modifies Drizzle schema files but forgets to generate the corresponding migration.
How it works:
- The
typecheckjob runsbun run db:generatewith a dummyDATABASE_URL - It checks
git diffon theapps/api/drizzle/directory - If uncommitted migration files are produced, CI fails
Error message you will see:
Schema drift detected! Your Drizzle schema has changes that are not captured in a migration file.
Run 'cd apps/api && bun run db:generate' locally and commit the generated migration.To fix: Run cd apps/api && bun run db:generate locally, review the generated SQL, and commit it with your schema changes.
Preview Database Branches
When deploying API previews, the Deploy Preview workflow creates an isolated Neon database branch so preview environments do not share the production database.
| Step | What happens |
|---|---|
deploy-api job — Neon branch step | Creates (or recreates) a Neon branch named preview/{branch} with its own DATABASE_URL, then resets the schema for clean migrations |
deploy-api job — deploy step | Builds the API with the branch DATABASE_URL, runs db:migrate during build |
| After review | Re-run the workflow with target cleanup to delete the Neon branch |
Cleanup:
- Go to Actions > Deploy Preview
- Click Run workflow
- Select the same branch used for the preview
- Choose target: cleanup
- Click Run workflow
The workflow deletes the Neon branch named preview/{branch}. You can also delete branches directly from the Neon console.
Database Initialization
On a fresh database (new Neon project, empty PostgreSQL instance), you must initialize the schema before migrations can run. This is due to a chicken-and-egg dependency between Better Auth and Drizzle.
The Better Auth + Drizzle Problem
Better Auth creates its core tables (users, sessions, accounts, verifications) at runtime via the Drizzle adapter — these tables are NOT defined in the migration files. Drizzle migrations (RLS policies, added columns, indexes) ALTER these tables and assume they already exist. On a fresh database, migrations fail because the tables they reference have not been created yet.
How to Initialize
Option A: Push the full schema (recommended)
Use drizzle-kit push --force to create all tables from the Drizzle schema definition, then run migrations to register them:
cd apps/api
DATABASE_URL="<your-production-url>" bunx drizzle-kit push --force
DATABASE_URL="<your-production-url>" bun run db:migrateThe --force flag skips interactive confirmation prompts. The db:migrate step registers all existing migration files as "already applied" so they are not re-run on the next deploy.
Option B: Let Better Auth bootstrap tables
Start the application once with a valid DATABASE_URL to let Better Auth create its tables at runtime, then run migrations:
# Start the API locally against production (or let Vercel deploy once — the build will fail on migrations, but Better Auth tables will exist)
DATABASE_URL="<your-production-url>" bun run dev
# Then apply migrations
DATABASE_URL="<your-production-url>" bun run db:migrateOption A is preferred because it is a single atomic step and does not require running the app.
Neon Preview Branches and Production State
Neon database branching creates a copy-on-write snapshot of the parent branch (production). If production is empty, every preview branch inherits that empty state and migrations fail with missing-table errors. Production must be initialized first before preview branches work correctly.
Important: If you see migration errors like
relation "users" does not existin preview deploys or production builds, your database has not been initialized. Rundrizzle-kit push --forceagainst the production database as described above.
Custom Domains
cd apps/web && vercel domains add app.yourdomain.com
cd apps/api && vercel domains add api.yourdomain.comThen configure DNS with a CNAME record pointing to cname.vercel-dns.com. Vercel handles SSL certificates automatically.
Deployment Protection
Both Vercel projects have SSO deployment protection enabled by default:
{
"ssoProtection": {
"deploymentType": "all_except_custom_domains"
}
}This requires Vercel team SSO authentication to access any deployment URL that is not a custom domain. The staging branch has custom domains (app.staging.roxabi.com, api.staging.roxabi.com) which bypass SSO. Other preview deployments use Vercel-generated URLs and are gated behind SSO login.
Options for Preview Access
| Option | Behavior | Trade-off |
|---|---|---|
| A. Relax protection to production only | Previews become publicly accessible | Anyone with the URL can access previews |
| B. Protection Bypass for Automation | Web→API proxy bypasses SSO transparently; CI health checks pass; humans still need SSO | Recommended — most secure option that still allows automated previews |
| C. Accept the default | All previews require SSO login | Most secure, but preview deployments cannot communicate with each other |
Option A — Make previews public:
TOKEN="<your-vercel-token>"
TEAM_ID="<your-team-id>"
# Update web project
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<web-project-id>?teamId=$TEAM_ID" \
-H "Content-Type: application/json" \
-d '{"ssoProtection":{"deploymentType":"prod_deployment_urls_only"}}'
# Update API project
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
"https://api.vercel.com/v9/projects/<api-project-id>?teamId=$TEAM_ID" \
-H "Content-Type: application/json" \
-d '{"ssoProtection":{"deploymentType":"prod_deployment_urls_only"}}'Option B — Protection Bypass for Automation (recommended):
The Deploy Preview workflow integrates with Vercel's Protection Bypass for Automation to allow the web app's Nitro proxy to forward requests to the SSO-protected API preview without requiring team authentication.
How it works:
- Generate a bypass secret on the API project: Vercel dashboard → API project → Settings → Deployment Protection → Protection Bypass for Automation → Generate
- Add it as a GitHub Actions secret:
VERCEL_AUTOMATION_BYPASS_SECRET(used by the health check step) - Add it as a Vercel preview env var on the web project:
vercel env add VERCEL_AUTOMATION_BYPASS_SECRET preview --cwd apps/web - The Nitro server route (
server/routes/api/[...path].ts) reads the secret at runtime fromprocess.envand addsx-vercel-protection-bypassto every outgoing/api/**request (server-side, invisible to clients). The secret is never baked into the build artifact. - The health check step also uses the secret to verify API preview accessibility
The bypass secret is only forwarded in preview environments (VERCEL_ENV === 'preview'), so local dev and production deployments are unaffected.
Option C — Keep the default: No configuration needed. All team members log in via Vercel SSO to access preview URLs, but web→API communication in previews will fail (401 errors).
Important Notes
- Vercel automatically sets
NODE_ENV=production - NestJS runs as a Vercel Function with Fluid compute — auto-scaling, reduced cold starts
- The API entrypoint is
src/index.ts(not the NestJS defaultsrc/main.ts), configured viaentryFileinnest-cli.json - NestJS middlewares with the Fastify adapter receive raw Node.js
ServerResponseobjects — useres.setHeader(), notres.header() - Free tier: 10s execution limit per request. Pro tier: 60s
- 250MB max application size (Vercel Functions limitation)
API_URL,APP_URL, andVITE_*variables are declared inturbo.jsoncbuild env to ensure Turbo invalidates cache when they change- Runtime secrets (database, auth, rate limiting) are declared as
globalPassThroughEnvin rootturbo.jsonc— these are available at runtime but don't affect build cache
Troubleshooting Marketplace Integrations
| Symptom | Cause | Fix |
|---|---|---|
| Env var not showing after integration install | Vercel requires a redeploy for new env vars to take effect | Run vercel redeploy <latest-deployment-url> |
| Manual env var conflicts with integration var | Both exist — manual var may take precedence | Remove the manual var: vercel env rm <VAR> production and redeploy |
| Preview deploy connects to production database | --env override missing or incorrect in workflow | Check deploy-preview.yml — the --env "DATABASE_URL=$DATABASE_URL" line must be present in the "Deploy API preview" step |
Integration shows in dashboard but var missing in vercel env ls | Integration not linked to the correct project | Uninstall and reinstall, ensuring you select the API project |
POSTGRES_* vars appear after Neon install | Neon integration injects extra vars | Harmless — code only reads DATABASE_URL. Ignore extra vars. |
VPS Alternative
The repository includes Docker configurations for VPS-based deployment:
- Multi-stage Dockerfiles for API and web
docker-compose.prod.ymlfor container orchestration- Nginx reverse proxy configs in
deploy/nginx/
If you have a VPS with Docker, you can use the Docker build/push/SSH deploy pipeline instead of Vercel.