niblr

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

  1. Create a project at https://supabase.com (free tier works).
  2. In Project Settings → Database → Connection string, select Transaction pooler and copy the connection string. It ends with :6543/postgres. This is your DATABASE_URL.
  3. 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:

  1. In your Supabase dashboard go to Storage → S3 Connection.
  2. Click Generate new credentials → copy the Access Key ID and Secret Access Key.
  3. Note your project reference ID (visible in the URL app.supabase.com/project/<ref>).
  4. The S3 endpoint URL is https://<ref>.supabase.co/storage/v1/s3.
  5. 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

  1. Go to https://vercel.com/new.
  2. Select your Git provider, find this repository, and click Import.
  3. Vercel auto-detects Next.js — leave the Framework Preset as Next.js.
  4. Do not add a build command override at this step (migrations run separately — see below).
  5. 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

VariablePurpose
DATABASE_URLSupabase transaction-pooler connection string (port 6543). Required for all Payload DB operations.
PAYLOAD_SECRETRandom 32-byte hex secret. Signs JWT sessions. Rotation logs all users out.
NEXT_PUBLIC_ROOT_DOMAINYour apex domain, e.g. yourdomain.com. Used by proxy.ts to distinguish tenant subdomains from custom hosts.
CREDENTIALS_ENCRYPTION_KEYRandom 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

VariablePurpose
S3_ENDPOINTS3-compatible endpoint URL, e.g. https://<ref>.supabase.co/storage/v1/s3.
S3_REGIONRegion string. For Supabase Storage use auto. For AWS use e.g. us-east-1.
S3_BUCKETBucket name (e.g. media). Must already exist in Supabase Storage.
S3_ACCESS_KEY_IDS3 access key generated in Supabase Storage → S3 Connection.
S3_SECRET_ACCESS_KEYS3 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).

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.

  1. Cloudflare → R2 → create a bucket (e.g. open-commerce-media) and an API token (Access Key ID + Secret).
  2. Set these env vars in Vercel (Production) and local .env:
    • S3_BUCKET=open-commerce-media
    • S3_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
    • S3_REGION=auto
    • S3_ACCESS_KEY_ID=...
    • S3_SECRET_ACCESS_KEY=...
  3. 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.
  4. Re-upload existing media so the bytes land in R2. For SD Bakery: pnpm reset:sdbakery && pnpm seed:sdbakery. After a storage backend change, run pnpm recompute:usage to true up mediaBytesUsed.

Email (transactional)

VariablePurpose
RESEND_API_KEYPlatform-level Resend API key. Used for order confirmation emails and other transactional messages originating from the platform (not tenant campaigns).
RESEND_FROM_EMAILVerified 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

VariablePurpose
CRON_SECRETAuth 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 Authorization header 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)

VariablePurpose
VERCEL_API_TOKENVercel 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_IDThe 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.

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:

  1. yourdomain.com (apex)
  2. *.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 that 301/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_SECRET must 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.ts function has maxDuration: 60 already set in vercel.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/domains which triggers the Vercel adapter (src/lib/domains/vercel.ts).
  • The adapter calls Vercel's POST /v10/projects/{id}/domains and returns verification DNS records.
  • The domain status stays pending until a super-admin marks it verified (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 seed run once against production DB to create demo users (or create a super-admin via the Payload admin UI at https://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 verified after DNS setup.
  • Change all seeded passwords before going live.