Deploying to Vercel
A full production deploy of Niblr on Vercel with Supabase for database and object storage — env vars, migrations, wildcard subdomains, and cron.
This guide walks through a full production deploy of Niblr on Vercel with Supabase as the database and object storage backend.
Prerequisites
1. Supabase project
- Create a project at https://supabase.com (free tier works).
- In Project Settings → Database → Connection string, select Transaction pooler and copy the connection string. It ends with
:6543/postgres. This is yourDATABASE_URL. - Keep the project dashboard open — you will return for S3 credentials (step below).
2. Generate secrets
Run these locally (or in any shell with openssl):
# Payload session secret — keep this private; rotation logs everyone out
openssl rand -hex 32 # → PAYLOAD_SECRET
# Encryption key for gateway secrets and unsubscribe tokens
openssl rand -hex 32 # → CREDENTIALS_ENCRYPTION_KEY
# Cron auth token — protects /api/marketing/process
openssl rand -hex 32 # → CRON_SECRET
3. Provision Supabase Storage S3 credentials
Supabase Storage exposes an S3-compatible API. To switch media uploads from local disk to Supabase Storage:
- In your Supabase dashboard go to Storage → S3 Connection.
- Click Generate new credentials → copy the Access Key ID and Secret Access Key.
- Note your project reference ID (visible in the URL
app.supabase.com/project/<ref>). - The S3 endpoint URL is
https://<ref>.supabase.co/storage/v1/s3. - Create a bucket (e.g.
media) and make it public if you want direct media URLs.
Without
S3_*vars: media falls back to local disk. On Vercel's ephemeral filesystem this means media does not persist across deploys. Always set S3 vars in production.
Import the Repository into Vercel
- Go to https://vercel.com/new.
- Select your Git provider, find this repository, and click Import.
- Vercel auto-detects Next.js — leave the Framework Preset as Next.js.
- Do not add a build command override at this step (migrations run separately — see below).
- Click Deploy — the first build will succeed and give you a preview URL. Configure env vars next.
Environment Variables
In Vercel → Project → Settings → Environment Variables, add each variable below. Set scope to Production (and Preview if you want preview deploys to use the same DB — normally use a separate Supabase project for staging).
Core
| Variable | Purpose |
|---|---|
DATABASE_URL | Supabase transaction-pooler connection string (port 6543). Required for all Payload DB operations. |
PAYLOAD_SECRET | Random 32-byte hex secret. Signs JWT sessions. Rotation logs all users out. |
NEXT_PUBLIC_ROOT_DOMAIN | Your apex domain, e.g. yourdomain.com. Used by proxy.ts to distinguish tenant subdomains from custom hosts. |
CREDENTIALS_ENCRYPTION_KEY | Random 32-byte hex key. AES-256-GCM encryption for gateway secrets (Stripe/Razorpay keys) and HMAC unsubscribe tokens. Losing this orphans all stored payment credentials — there is no re-encryption path. |
Storage / S3
| Variable | Purpose |
|---|---|
S3_ENDPOINT | S3-compatible endpoint URL, e.g. https://<ref>.supabase.co/storage/v1/s3. |
S3_REGION | Region string. For Supabase Storage use auto. For AWS use e.g. us-east-1. |
S3_BUCKET | Bucket name (e.g. media). Must already exist in Supabase Storage. |
S3_ACCESS_KEY_ID | S3 access key generated in Supabase Storage → S3 Connection. |
S3_SECRET_ACCESS_KEY | S3 secret key generated in Supabase Storage → S3 Connection. |
When all five S3_* vars are set, payload.config.ts switches the media adapter from local disk to @payloadcms/storage-s3. Unset any one of them and the build falls back to local disk (useful for preview branches without S3).
Object storage — Cloudflare R2 (recommended)
Media uses the S3 adapter (payload.config.ts, gated on S3_BUCKET). R2 is S3-compatible and forcePathStyle is already set, so no code change is needed.
- Cloudflare → R2 → create a bucket (e.g.
open-commerce-media) and an API token (Access Key ID + Secret). - Set these env vars in Vercel (Production) and local
.env:S3_BUCKET=open-commerce-mediaS3_ENDPOINT=https://<account_id>.r2.cloudflarestorage.comS3_REGION=autoS3_ACCESS_KEY_ID=...S3_SECRET_ACCESS_KEY=...
- Redeploy to activate the adapter. Media serves through Payload's
/api/media/file/…route, so the bucket stays private — no public access or custom domain needed. - Re-upload existing media so the bytes land in R2. For SD Bakery:
pnpm reset:sdbakery && pnpm seed:sdbakery. After a storage backend change, runpnpm recompute:usageto true upmediaBytesUsed.
Email (transactional)
| Variable | Purpose |
|---|---|
RESEND_API_KEY | Platform-level Resend API key. Used for order confirmation emails and other transactional messages originating from the platform (not tenant campaigns). |
RESEND_FROM_EMAIL | Verified Resend sender address for platform emails (e.g. noreply@yourdomain.com). |
Tenant-level campaign emails use each tenant's own resendApiKey stored in Marketing Configs (encrypted at rest with CREDENTIALS_ENCRYPTION_KEY).
Cron
| Variable | Purpose |
|---|---|
CRON_SECRET | Auth token for POST /api/marketing/process. Vercel injects it automatically as Authorization: Bearer $CRON_SECRET on every cron invocation (Pro/Enterprise only). Self-hosters pass it via x-cron-secret header. |
Vercel Hobby plan note: Vercel does not inject the
Authorizationheader on Hobby-plan crons. The endpoint silently returns 401 on every tick. Upgrade to Pro/Enterprise or trigger the endpoint manually with-H "x-cron-secret: <value>".
Custom Domains (optional)
| Variable | Purpose |
|---|---|
VERCEL_API_TOKEN | Vercel personal/team API token. When set, adding a domain in the merchant admin automatically registers it with your Vercel project via the v10 domains API. Without it, the app falls back to the manual adapter and shows DNS records for the merchant to set themselves. |
VERCEL_PROJECT_ID | The Vercel project ID (visible in Project → Settings → General). Required when VERCEL_API_TOKEN is set. |
Run Database Migrations (Release Step)
Do NOT run migrations inside the Next.js build command. Concurrent preview builds can race against the same database and cause migration conflicts. Run migrations as a dedicated release step or post-deploy hook.
Option A — Vercel Release Command (recommended)
In Vercel → Project → Settings → General → Build & Development Settings, set:
- Build Command:
pnpm build(default — leave as-is) - Install Command:
pnpm install(default)
Then add a Release Command (Vercel runs this once per deployment, after build, before traffic shifts):
pnpm payload migrate
The release command inherits all env vars, so it connects to the same DATABASE_URL as the running app.
Option B — Run manually before first deploy
# From your local machine with the production DATABASE_URL set:
DATABASE_URL=<your-url> PAYLOAD_SECRET=<your-secret> pnpm payload migrate
Then push/deploy normally. On subsequent deploys that include new migrations, re-run this command (or promote Option A).
Configure Wildcard Subdomains
In Vercel → Project → Settings → Domains, add:
yourdomain.com(apex)*.yourdomain.com(wildcard)
Follow Vercel's DNS instructions to point the records at Vercel. Vercel provisions wildcard TLS automatically. Each tenant at {slug}.yourdomain.com is routed by proxy.ts to the correct storefront.
Wildcard SSL note: wildcard certificates cover one level of subdomains (e.g.
store-a.yourdomain.com) but not deeper paths (e.g.a.b.yourdomain.com). Tenant slugs must not contain dots.
Canonical domain: make the apex primary
NEXT_PUBLIC_ROOT_DOMAIN is your apex (e.g. yourdomain.com), and every canonical URL the app emits — the sitemap, <link rel="canonical"> tags, and JSON-LD — is built from it. It also has to be the apex, because tenant stores live at {slug}.yourdomain.com.
So configure the redirect to point the same way: in Vercel → Project → Settings → Domains, set yourdomain.com as the primary domain and have www.yourdomain.com redirect to it. If you do the reverse (apex → www), every URL in your sitemap redirects before it resolves, which Google Search Console flags as "Page with redirect" and dilutes indexing.
Rule of thumb: the host in your canonical URLs must be the host that serves a
200, not one that301/308s. Since canonicals use the apex, the apex should be primary.
Marketing Cron
The vercel.json already declares a cron schedule:
{
"crons": [{ "path": "/api/marketing/process", "schedule": "* * * * *" }]
}
For this to work:
CRON_SECRETmust be set in Vercel env vars.- The Vercel project must be on Pro or Enterprise (Hobby does not inject the auth header).
- The
src/app/api/marketing/process/route.tsfunction hasmaxDuration: 60already set invercel.json.
Custom Domain Auto-Attach
When VERCEL_API_TOKEN and VERCEL_PROJECT_ID are set:
- Adding a domain in the merchant admin calls
POST /api/domainswhich triggers the Vercel adapter (src/lib/domains/vercel.ts). - The adapter calls Vercel's
POST /v10/projects/{id}/domainsand returns verification DNS records. - The domain status stays
pendinguntil a super-admin marks itverified(or a future DNS polling step does so automatically).
Without these two vars the manual adapter is used — the app still shows the DNS records the merchant must set, but no Vercel API call is made.
Post-Deploy Checklist
-
pnpm seedrun once against production DB to create demo users (or create a super-admin via the Payload admin UI athttps://yourdomain.com/admin). - Wildcard domain shows in Vercel Domains with green SSL status.
-
curl -H "Host: <tenant-slug>.yourdomain.com" https://yourdomain.com/rewrites to the tenant storefront. - Platform admin (
https://yourdomain.com/admin) accessible with super-admin credentials. - A custom domain added by a tenant reaches
verifiedafter DNS setup. - Change all seeded passwords before going live.