Deployment
This guide covers everything you need to take SaaSyBase from local development to production — whether you're deploying to Vercel, self-hosting on a VPS, or using a platform like Coolify.
Before Your First Deploy
Complete these steps after local development is finished and before you point real traffic at the app:
Warning
npm run build to generate some pages, so missing or broken env vars can fail the build before the app ever starts.Note
npm install for you during the build, so those guides focus on the commands you still must think about yourself. Database seeding is different: it is a one-time bootstrap step for a brand-new database when you want the starter plans, settings, and editable site pages. It should not run automatically on every deploy.Pre-flight checklist
Check Node.js Version
Pin your runtime to a supported Node.js version: ^20.19.0, ^22.12.0, or >=24.0.0. Check the exact version with node --version before the first deploy. Older versions like Node 18 will fail with modern Next.js and runtime API expectations.
Provision PostgreSQL
Create a hosted PostgreSQL database (e.g., Neon, Supabase, Railway) and update your DATABASE_URL environment variable.
Configure Environment Variables
Set all production env vars for your chosen auth provider, payment provider, email delivery, and server-side secrets.
Verify the production connection string
From the same environment that will run Prisma, confirm DATABASE_URL points at the intended PostgreSQL instance and that the credentials are valid. Do this before migrations so a bad connection string does not look like a Prisma or deployment failure.
Run Production Migrations
Apply your database schema to the new production database after DATABASE_URL is confirmed.
npm run prisma:deploySeed only if this is a brand-new database
Run the seed script one time if you want the starter admin, seeded plans, default settings, and editable site pages. Skip this on normal repeat deploys so you do not confuse setup with everyday releases.
npx prisma db seedConfigure Webhooks
Point your auth and payment provider webhooks to your new production domain and verify signatures are working.
Rotate Local Secrets
Generate new, secure random strings for ENCRYPTION_SECRET and other tokens. Never use your local development keys in production.
Warning
npm run prisma:deploy or committed migrations.PostgreSQL options
Any normal PostgreSQL connection string works with Prisma. You do not need a special vendor integration.
| Option | Good fit when | Docs |
|---|---|---|
| Neon | You want a simple serverless Postgres default | Neon docs |
| Supabase | You want Postgres plus extra platform features | Supabase docs |
| Railway | You want one place for app plus database hosting | Railway docs |
| Render | You already deploy infrastructure on Render | Render docs |
| Self-hosted PostgreSQL | You want full control on your own machine or VPS | PostgreSQL downloads |
Common connection string shape:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DBNAME?schema=public"Tip
Note
Required Environment Variables
# Core
DATABASE_URL="postgresql://..."
NEXT_PUBLIC_APP_URL="https://yourdomain.com"
NEXT_PUBLIC_APP_DOMAIN="yourdomain.com"
NEXT_PUBLIC_SITE_NAME="Your App"
# Auth (pick one)
AUTH_PROVIDER="betterauth" # or "nextauth" or "clerk"
# Payment (pick one)
PAYMENT_PROVIDER="stripe"
# Security
ENCRYPTION_SECRET=""
INTERNAL_API_TOKEN=""
HEALTHCHECK_TOKEN=""
CRON_PROCESS_EXPIRY_TOKEN=""
# Email
EMAIL_PROVIDER="nodemailer" # or "resend"
SMTP_HOST=""
SMTP_PORT=""
SMTP_USER=""
SMTP_PASS=""
EMAIL_FROM=""
SUPPORT_EMAIL=""Note
Tip
DATABASE_URL, auth config, and server secrets.Recommended secret setup
For most teams, the safest low-friction rule is: keep .env.localfor your laptop, and use your hosting platform's encrypted env vars in staging and production.
Standard secrets workflow
Use .env.local for development
Keep a local .env.local file on your laptop for rapid development and testing.
Set platform secrets
Set the real secrets in your platform env settings (Vercel, Coolify, etc.) first.
Optional: Centralize with a provider
If you want centralized secret management across platforms, opt into SECRETS_PROVIDER=infisical or SECRETS_PROVIDER=doppler. Install and authenticate the provider CLI in the real build/runtime environment first, then follow the dedicated Secrets & Providers guide for the provider-specific setup steps.
Verify environment
Run the smoke test to verify all required variables are present before your first deploy.
npm run secrets:smokeDeploy
Deploy normally using your platform's standard migration, build, and start commands.
npm run prisma:deploy
npm run build
npm run start# Leave blank to use platform-native envs only
SECRETS_PROVIDER="infisical"
# Optional override when you need a custom export command
SECRETS_PROVIDER_COMMAND=""
# Provider-specific hints
INFISICAL_PROJECT_ID=""
INFISICAL_ENVIRONMENT=""Before the first real deploy, you can also run npm run secrets:doctor to see the exact provider command, detected output shape, and which expected keys are present before the app boots.
Tip
Note
Optional Sentry setup
SaaSyBase already keeps built-in operator logs in SystemLog and exposes them at /admin/logs. Sentry is optional and additive: when enabled, production logger events and React error boundaries also forward to Sentry.
Note
SENTRY_ENABLED="true", SENTRY_DSN, and SENTRY_ENVIRONMENT. Add NEXT_PUBLIC_SENTRY_DSN only if you also want browser crash reporting.# Server-side only
SENTRY_ENABLED="true"
SENTRY_CAPTURE_IN_DEVELOPMENT="false"
SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"
SENTRY_ENVIRONMENT="production"
SENTRY_RELEASE="git-sha-or-image-tag"
# Optional browser crash reporting
NEXT_PUBLIC_SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"If you want to verify logger fan-out locally from the admin settings UI, set SENTRY_CAPTURE_IN_DEVELOPMENT="true" and use the Sentry smoke-test controls in /admin/settings.
Health Check
GET /api/health
Authorization: Bearer <HEALTHCHECK_TOKEN>Returns database connectivity, environment validation, active auth/payment provider diagnostics, and runtime health. Without authorization, the endpoint returns a minimal public response — useful for uptime monitors that just need a 200 OK.
Use the authorized response immediately after a deploy and before pointing real traffic at the app. It is the fastest way to confirm the database, provider config, and runtime env all look correct from the deployed process.
In production, detailed health output requires a dedicated HEALTHCHECK_TOKEN.
Updating an existing live install
If your app is already live and customized, treat new official SaaSyBase releases as controlled upgrades instead of fresh installs. In most cases these releases should be normal maintenance: improvements, bug fixes, security fixes, and polish.
Note
- Read the release summary first so you know whether this is a routine update or one with infrastructure changes.
- Merge and review the update in Git first, not on the production server.
- Back up the live database and confirm the effective production env configuration before rollout.
- Run the update in staging with production-like providers and storage before touching production.
- Deploy production with the same ordered flow every time:
npm run prisma:deploy, then build, then start or promote.
Vercel Deployment
Make sure the project uses a supported Node.js runtime in the Vercel project settings. Do not rely on older defaults such as Node 18.
Note
npm install step in the Vercel guide unless you are debugging in a shell outside the normal Vercel build flow.Vercel deployment steps
Import project to Vercel
Import your repository into Vercel. Vercel will automatically detect that it is a Next.js project.
Set environment variables
Add all your production environment variables in the Vercel project settings.
Deploy database schema
Run the Prisma deploy command against your production PostgreSQL database before the first live release.
npm run prisma:deploySeed once if the database is empty
Run the seed script one time against the same production database only if you want the starter records. This is not part of every Vercel deploy.
npx prisma db seedConfigure durable uploads when needed
If users or admins will upload files, configure S3-compatible storage by setting FILE_STORAGE="s3" and the related credentials. On Vercel, persistent app-managed uploads should be treated as required S3 storage because the local filesystem is not a durable production store.
Enable background jobs
Set CRON_SECRET to a secure random string. The cron endpoint also accepts CRON_PROCESS_EXPIRY_TOKEN or CRON_TOKEN, but CRON_SECRET is the simplest default for the bundled Vercel scheduler.
Deployment checklist
DATABASE_URLpoints at PostgreSQLNEXT_PUBLIC_APP_URLmatches the production domain exactlyAUTH_PROVIDERandPAYMENT_PROVIDERare set deliberately- You understand Vercel handles dependency install automatically during build
- You ran
npx prisma db seedonly if this was a brand-new empty database CRON_SECRETis configured for the bundled cronFILE_STORAGE="s3"if you need durable uploads- Clerk and payment webhooks point at the production domain
Tip
vercel.json schedules the expiry cron once per day at 03:00 UTC. This is intentionally conservative so it works on Vercel Hobby plans. Increase frequency if your plan supports it. With the default schedule, subscription cleanup can lag by up to about 24 hours.If you want Vercel to fetch secrets through the built-in bootstrap instead of native Vercel env vars, use the Secrets & Providers guide.
Optional Sentry on Vercel
Add the Sentry env vars in the Vercel project environment settings for Production and Preview. If you want browser crash capture, set both the server DSN and the public DSN.
SENTRY_ENABLED="true"
SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"
SENTRY_ENVIRONMENT="production"
SENTRY_RELEASE="vercel-${VERCEL_GIT_COMMIT_SHA}"
NEXT_PUBLIC_SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"Tip
NEXT_PUBLIC_SENTRY_DSN blank if you only want server-side monitoring. That avoids shipping any browser Sentry config to clients.Production webhooks
Point production webhook providers at your deployed domain:
| Integration | Endpoint | Required secret |
|---|---|---|
| Clerk | /api/webhooks/clerk | CLERK_WEBHOOK_SECRET |
| Stripe | /api/webhooks/payments or /api/webhooks/stripe or /api/stripe/webhook | STRIPE_WEBHOOK_SECRET |
| Paystack | /api/webhooks/payments or /api/webhooks/paystack | PAYSTACK_WEBHOOK_SECRET (optional, falls back to PAYSTACK_SECRET_KEY) |
| Paddle | /api/webhooks/payments or /api/webhooks/paddle | PADDLE_WEBHOOK_SECRET |
| Razorpay | /api/webhooks/payments | RAZORPAY_WEBHOOK_SECRET |
Support ticket emails
If you want ticket activity to generate emails, set SUPPORT_EMAIL and make sure your email provider is configured. The support system can emit three admin/user email events:new_ticket_to_admin, admin_reply_to_user, and user_reply_to_admin.
Coolify Deployment
Coolify can deploy SaaSyBase as a standard Node/Next.js app without a custom Dockerfile:
Note
^20.19.0, ^22.12.0, or >=24.0.0 before the first deploy.Note
Coolify deployment steps
Connect repository
Connect your repository to Coolify as a standard Node or Nixpacks-style application.
Set environment variables and runtime
Add your production env vars in the Coolify application settings and pin the runtime to a supported Node.js version before the first deploy. At minimum, configure DATABASE_URL, NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_APP_DOMAIN, NEXT_PUBLIC_SITE_NAME, AUTH_PROVIDER, PAYMENT_PROVIDER, ENCRYPTION_SECRET, INTERNAL_API_TOKEN, HEALTHCHECK_TOKEN, CRON_PROCESS_EXPIRY_TOKEN, and your email/provider secrets.
Use a build-safe PostgreSQL URL
If your DATABASE_URL includes a file-based CA path like sslrootcert=/etc/ssl/certs/coolify-ca.crt, the build container may fail if that file is not present there. Prefer a URL that also works in the build container unless you are deliberately shipping that certificate into both build and runtime images.
Configure a pre-deploy migration step
Use Coolify's deployment hooks or pre-deploy command field so every release applies migrations automatically before the app is started. A one-time manual run is fine for rescue work, but it is not enough for an ongoing production workflow.
npm run prisma:deploySeed once if this is the first launch of a new database
Run the seed script a single time if you want the starter admin, plans, settings, and site pages. Do not wire seeding into every deployment hook.
npx prisma db seedSet build and start commands
Configure the exact commands Coolify should use after migrations succeed.
Build: npm run deploy:build
Start: npm run startConfigure production storage if uploads must persist
If the app will store user-managed uploads, use S3-compatible storage instead of the container filesystem so assets survive container rebuilds, restarts, and reschedules.
Configure background jobs
Create a scheduled HTTP job in Coolify to hit the expiry cron endpoint hourly or daily. The route accepts CRON_PROCESS_EXPIRY_TOKEN, CRON_SECRET, or CRON_TOKEN; pick one name and use it consistently.
curl -i -m 60 \
+ -H "Authorization: Bearer $CRON_PROCESS_EXPIRY_TOKEN" \
+ "https://yourdomain.com/api/cron/process-expiry"Validate the deployed service before cutover
After deploy, call the authorized health endpoint and verify the app is running with the expected database, auth, payment, and env configuration before sending traffic to it.
If you want centralized secret loading here instead of raw Coolify env vars, see the Secrets & Providers guide.
# Build command
npm run deploy:build
# Start command
npm run startTip
npm run deploy:build is the recommended Coolify build command for this repo because it applies committed migrations first and then runs the normal production build. Treat the migration hook, build command, and start command as one ordered deployment flow: migrate first, then build, then start.Note
Optional Sentry on Coolify
In Coolify, add the same env vars to the application service. Use a stable SENTRY_ENVIRONMENT value like staging or production so issues group cleanly.
SENTRY_ENABLED="true"
SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"
SENTRY_ENVIRONMENT="staging"
SENTRY_RELEASE="coolify-build-2026-04-22"
# Optional browser-side capture
NEXT_PUBLIC_SENTRY_DSN="https://abc123@o000000.ingest.sentry.io/0000000"Linux VPS (Nginx / Apache)
Install and pin a supported Node.js runtime on the host before you build or start the app. Match the version policy in package.json.
Deployment flow
npm install
npm run secrets:smoke
npm run prisma:deploy
npm run build
npm run startRun the app under systemd, pm2, or another process manager. systemd is the simplest default on most Linux VPS hosts.
Note
npm install is explicit because you manage the machine yourself. Add npx prisma db seed right after npm run prisma:deploy only on the first setup of an empty database.Environment variable methods
Option 1 — systemd EnvironmentFile (recommended):
HEALTHCHECK_TOKEN=<generated>
DATABASE_URL=postgresql://...
# ...other varsReference in your service unit:
[Service]
EnvironmentFile=/etc/saasybase/app.env
ExecStart=/usr/bin/npm run start
WorkingDirectory=/var/www/saasybaseOption 2 — dotenv alongside the app:
set -a; source .env.production; set +a
npm run startIf you want a VPS to fetch secrets from a centralized store instead of a local env file, see the Secrets & Providers guide.
Optional Sentry envs on a VPS:
SENTRY_ENABLED=true
SENTRY_DSN=https://abc123@o000000.ingest.sentry.io/0000000
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE=$(git rev-parse HEAD)
# Optional browser-side capture
NEXT_PUBLIC_SENTRY_DSN=https://abc123@o000000.ingest.sentry.io/0000000If you use systemd, keep these alongside your other app env vars in the same environment file. If you deploy with Docker Compose or another process manager, pass the same values through that runtime instead.
Nginx reverse proxy
server {
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
}Pair with TLS via Let's Encrypt or your existing certificate automation.
Apache reverse proxy
<VirtualHost *:80>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>Enable the required proxy modules (proxy, proxy_http,headers) and terminate TLS in your HTTPS virtual host.
Cron job
curl -i -m 60 -H "Authorization: Bearer $CRON_PROCESS_EXPIRY_TOKEN" "https://yourdomain.com/api/cron/process-expiry"Schedule this to run hourly or daily depending on how quickly you need expired subscriptions to be cleaned up. If you standardize on a different alias, the route also accepts CRON_SECRET and CRON_TOKEN.
Deployment troubleshooting
- If
npm run prisma:deployfails, verifyDATABASE_URLfrom the same runtime context that will run the deploy. Most first-deploy Prisma errors are bad hostnames, wrong credentials, or the wrong database target. - If Prisma raises
P3019, you are mixing provider lanes. In this repo the committed migration history is PostgreSQL, so do not try to deploy an old SQLite migration chain or SQLite database state into PostgreSQL. - If the app starts locally but not in production, use the authorized
/api/healthresponse to confirm provider config and required env vars before digging through platform logs. - If uploads appear to work and then disappear after a deploy or restart, move production storage to
FILE_STORAGE="s3"with durable bucket credentials. - If cron-driven cleanup does not run, confirm the bearer token name matches one of the accepted aliases and test the endpoint manually before relying on the scheduler.
- If webhooks stop after a deploy, verify the provider dashboard is pointing at the production domain and that the matching webhook secret env var was updated in the deployed environment.
Cron Jobs & Expiry Automation
curl -i -m 60 -H "Authorization: Bearer $CRON_PROCESS_EXPIRY_TOKEN" "https://yourdomain.com/api/cron/process-expiry"Run this periodically to:
- Expire stale ACTIVE subscriptions past their
expiresAtdate. - Apply the configured organization-expiry policy to "zombie" organizations whose owner's subscription has lapsed. The shipped default is suspension rather than dismantling.
- Process the subscription queue for batch operations.
The route accepts any of these tokens: CRON_PROCESS_EXPIRY_TOKEN, CRON_SECRET, or CRON_TOKEN. Unauthorized requests return 404 in production.
Lazy fallback
File Storage (S3)
By default, uploaded files are stored on the local filesystem. Switch to S3 for production:
FILE_STORAGE="s3"
FILE_S3_BUCKET="my-bucket-name"
AWS_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
FILE_CDN_DOMAIN="" # Optional: CloudFront domain
FILE_S3_ENDPOINT="" # Optional: S3-compatible endpoint (R2, MinIO, etc.)When FILE_CDN_DOMAIN is set, uploads return CDN URLs instead of raw S3 links. Set FILE_S3_ENDPOINTto your provider's endpoint for S3-compatible services (Cloudflare R2, DigitalOcean Spaces, MinIO). Leave it blank for standard AWS S3.
Note
LOGO_STORAGE, LOGO_S3_BUCKET, LOGO_S3_ENDPOINT, and LOGO_CDN_DOMAINvariables as legacy aliases.S3 CORS for browser uploads
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedOrigins": ["https://yourdomain.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]Without a browser-friendly CORS policy, admin uploads can fail even when the bucket credentials are valid.
CloudFront or CDN fronting
For AWS-hosted setups, front the bucket with CloudFront and use:
- Response headers policy:
CORS-With-Preflight - Origin request policy:
CORS-S3Origin - Cache policy:
CachingOptimized - Allowed methods:
GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Also add your storage/CDN hostnames to next.config.mjs image remote patterns so branded images render correctly.
Demo Read-Only Mode
DEMO_READ_ONLY_MODE="true"
DEMO_READ_ONLY_EXEMPT_USER_IDS="user_123,user_456"
DEMO_READ_ONLY_EXEMPT_EMAILS="owner@example.com"When enabled, all write operations (POST, PUT, PATCH, DELETE) to /api/* are blocked with a 403 response. Auth and webhook callbacks are exempted so sign-in still works.
Use this for sharing a safe, explorable demo. A read-only modal appears in admin/dashboard, and blocked actions trigger an informational toast.
For a narrow operator bypass, allowlist exact internal user IDs or exact emails with DEMO_READ_ONLY_EXEMPT_USER_IDS and DEMO_READ_ONLY_EXEMPT_EMAILS. Prefer user IDs when possible because they are stable and avoid ambiguity if a user later changes email.
Environment Variable Reference
A complete list organized by group:
| Group | Key prefix / variables | Notes |
|---|---|---|
| Database | DATABASE_URL | Points Prisma at the target database. The committed provider lane in this repo is PostgreSQL. |
| App | NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_SITE_NAME, NEXT_PUBLIC_APP_DOMAIN | Public-facing URL and branding |
| Branding | NEXT_PUBLIC_SITE_LOGO, NEXT_PUBLIC_SITE_LOGO_LIGHT/DARK | Site logo configuration |
| Auth | AUTH_PROVIDER, CLERK_*, BETTER_AUTH_URL, NEXT_PUBLIC_BETTER_AUTH_URL, BETTER_AUTH_SECRET, AUTH_SECRET, NEXTAUTH_SECRET | Choose Clerk, Better Auth, or NextAuth |
| Auth OAuth | GITHUB_CLIENT_ID/SECRET, GOOGLE_CLIENT_ID/SECRET | OAuth providers for the self-hosted auth lanes |
| Payment | PAYMENT_PROVIDER, STRIPE_*, PAYSTACK_*, PADDLE_*, RAZORPAY_* | Choose provider |
| Payment config | PAYMENT_AUTO_CREATE, PAYMENTS_CURRENCY | Catalog sync and currency |
| Currency | DEFAULT_CURRENCY, PADDLE_CURRENCY, PAYSTACK_CURRENCY, RAZORPAY_CURRENCY | Currency resolution chain |
| EMAIL_PROVIDER, SMTP_*, RESEND_API_KEY, EMAIL_FROM, SUPPORT_EMAIL | Email delivery | |
| Geolocation | IPINFO_LITE_TOKEN | Optional; session geolocation |
| Storage | FILE_STORAGE, FILE_S3_BUCKET, FILE_S3_ENDPOINT, AWS_*, FILE_CDN_DOMAIN | File storage (legacy LOGO_* aliases still work) |
| Analytics | TRAFFIC_ANALYTICS_PROVIDER, NEXT_PUBLIC_GA_MEASUREMENT_ID, GA_*, POSTHOG_* | Traffic analytics provider configuration |
| Security | ENCRYPTION_SECRET, INTERNAL_API_TOKEN, HEALTHCHECK_TOKEN, CRON_* | Server-side secrets |
| Demo | DEMO_READ_ONLY_MODE | Read-only demo mode |
| Dev helpers | ALLOW_UNSIGNED_CLERK_WEBHOOKS, ALLOW_SYNC_IN_PROD | Break-glass local/dev helpers only |

