Environments
Development, preview, and production environment configuration. Vercel + Supabase branching model.
Environments
Purpose: How Framework manages development, preview, and production environments. Covers the three-environment model, secrets management via 1Password, deployment flow, and service-specific isolation strategies.
Environment strategy
Three-environment model
| Environment | Suffix | Purpose | URL | Who |
|---|---|---|---|---|
| Local | lcl |
Development | localhost:3000 | Developers |
| Preview | pre |
Regression testing before production | pr-123.vercel.app | Developers, reviewers |
| Production | prd |
Real users | app.product.com | Everyone |
Promotion flow: Local (lcl) → Preview (pre) → Production (prd).
- Develop locally — Push branch or merge to
main - Vercel auto-deploys Preview — Regression test on the preview URL
- Promote to Production — Via Vercel dashboard only
Pushing to main does NOT deploy to production.
Environment variables
| Prefix | Visibility | Exposed to browser? |
|---|---|---|
NEXT_PUBLIC_* |
Public, client-side | Yes |
| No prefix | Server-side only | No |
Never put secrets in NEXT_PUBLIC_ variables.
Backing services per environment
Each environment has its own isolated backing services. Local and Preview do NOT share databases.
| Service | Local (lcl) | Preview (pre) | Production (prd) |
|---|---|---|---|
| Supabase | {project}-lcl |
{project}-pre |
{project}-prd |
| Clerk | Dev instance (shared) | Dev instance (shared) | Production instance |
| Resend | Shared key (guarded) | Shared key (guarded) | Live mode |
| PostHog | Test project | Test project | Prod project |
| Stripe | Test mode | Test mode | Live mode |
Naming convention for Supabase projects: {project-name}-{env} (e.g., omni-access-lcl, omni-access-pre, omni-access-prd).
Service-specific environment strategies
Not every service supports three isolated environments. The strategies below document how each service maps to the three-environment model.
Clerk (Auth)
Clerk offers two tiers -- Development (pk_test_/sk_test_) and Production (pk_live_/sk_live_).
There is no preview tier.
| Environment | Clerk Instance | Keys |
|---|---|---|
| Local (lcl) | Dev instance (shared) | pk_test_ / sk_test_ |
| Preview (pre) | Dev instance (shared) | Same as lcl |
| Production (prd) | Production instance | pk_live_ / sk_live_ |
Resend (Email)
Resend has no sandbox, test mode, or per-environment isolation. All environments share one Resend account. Isolation is handled at two levels: code-level recipient guarding and API key scoping.
Environment isolation (code-level guard):
In non-production, the email client redirects all recipients to delivered@resend.dev (Resend's safe test address).
Real API calls are made so you can validate sending works, but no real inboxes are hit.
Only production (VERCEL_ENV=production) delivers to actual recipients.
// resend-client.ts
function safeRecipient(to: string): string {
if (process.env.NODE_ENV === "production" && process.env.VERCEL_ENV === "production") {
return to
}
return "delivered@resend.dev"
}
API key strategy:
Resend supports two permission levels per API key, with optional domain restriction:
| Key type | Permissions | Domain scope | Use for |
|---|---|---|---|
| Full Access | Send, manage resources | All domains | Local dev, admin |
| Sending Access | Send only | Restricted to one domain | Production, per-tenant |
For multi-tenant apps where tenants have their own sending domains, create a domain-scoped Sending Access key per tenant and store it in the database alongside EMAIL_FROM (tenant config, not env vars).
Viewing and previewing emails:
| Method | What you see | When to use |
|---|---|---|
pnpm email:dev |
React Email preview server (localhost:3001) | Designing/iterating on templates |
| Resend Dashboard > Emails | Rendered preview, HTML, plain text, delivery logs | After sending (including test sends to delivered@resend.dev) |
| Resend Dashboard > Share | Public link to a sent email (valid 48h) | Sharing with teammates for review |
Domain reputation: Never send test emails to fake addresses from your production domain.
The delivered@resend.dev guard prevents this -- test sends go through Resend's test infrastructure and don't affect your domain reputation.
Secrets management: 1Password
All secrets stored in 1Password -- one item per environment within a single project vault. 1Password is the single source of truth.
Vault and item structure
- One vault per project — Contains all environment items
- One item per environment — Stores the full
.envcontent innotesPlain(a single text block, not individual fields)
| Vault | Item | Maps to | When populated |
|---|---|---|---|
{project} |
{project}-lcl |
.env.local (local dev) |
M0: Manual Setup |
{project} |
{project}-pre |
Vercel Preview env | When preview environment is created |
{project} |
{project}-prd |
Vercel Production env | Pre-launch |
Layer 1: Local development -- copy from 1Password
Copy the notesPlain content from the 1Password item into .env.local:
- Open 1Password — Navigate to
{project}vault >{project}-lclitem - Copy the notes content — Contains all key=value pairs
- Paste into
.env.local— Gitignored, never committed
The .env.op file (committed) documents which vault/item maps to which environment.
It is a reference file, not used by op run.
Layer 2: CI -- GitHub Actions
- CI secrets — Configured as GitHub repository secrets or injected from Vercel environment variables
- Source of truth — The 1Password item defines which values to set
Layer 3: Vercel
Env vars are synced from 1Password to Vercel using pnpm sync:vercel-env:
pnpm sync:vercel-env wix-access-pre preview
pnpm sync:vercel-env wix-access-prd production
The script reads notesPlain from the 1Password item, extracts KEY=VALUE lines, and sets each one in Vercel using printf (not echo) to avoid trailing newlines.
[!WARNING] Never use
echo "value" | vercel env addmanually --echoappends a trailing newline that becomes part of the value and breaks length-validated vars. Always useprintf '%s' "value"or the sync script.
Layer 4: Typed validation -- lib/env.ts
- Zod schema — Validates every env var on first import
- Fails hard — Clear error listing missing vars
Summary: secret flow per environment
| Environment | 1Password Item | Mechanism |
|---|---|---|
| Local | {project}-lcl |
Copy notesPlain into .env.local |
| CI | {project}-lcl |
GitHub secrets / Vercel env |
| Vercel Preview | {project}-pre |
pnpm sync:vercel-env {project}-pre preview |
| Vercel Production | {project}-prd |
pnpm sync:vercel-env {project}-prd production |
Do: Store all secrets in 1Password notesPlain, one item per environment, copy into .env.local for local dev, validate with Zod.
Don't: Commit .env.local, reuse local keys in preview/production, share database between environments.
Deployment
Branch strategy
| Git Branch | Vercel Environment | Triggers |
|---|---|---|
main |
Preview | Every push to main |
| Feature branches | Preview | Every push |
production |
Production | Only when promoted via Vercel dashboard |
Vercel production branch is set to production, not main.
- Pushes to
main— Create Preview deployments (backed by-preservices), not Production deployments - Production is always explicit — Never an automatic deploy
productionbranch — Exists in the repo but is never pushed to directly; serves only as Vercel's production branch target- To deploy to production — Promote a known-good Preview deployment in the Vercel dashboard
Deploy flow
Feature branch pushed -> Vercel Preview deployment (backed by -pre services)
Merge to main -> Vercel Preview deployment (backed by -pre services)
Test on Preview -> Promote to Production in Vercel dashboard
Rollback procedure
If production breaks:
- Open Vercel Dashboard — Navigate to Project > Deployments
- Find last known good deployment — Review the deployment list
- Select "..." menu — Click "Promote to Production"
- Time — Under 60 seconds
Rule: Rollback first, debug second.
Database migrations (expand/contract)
Two-phase migrations prevent breaking running code during deploy:
- Expand (backward compatible) — Add new columns as nullable/with defaults, deploy code handling both schemas
- Contract (cleanup) — Remove old columns after confirming new schema works
npx prisma migrate dev --name descriptive_name # Local
# Preview/Production: npx prisma migrate deploy (via Vercel build)
- Apply to all three Supabase projects — Migrations must reach lcl, pre, and prd
- Never run
prisma migrate devagainst preview or production
Feature flags (PostHog)
- Use for — Large features, critical path changes, or features you might need to kill quickly
- Skip for — Bug fixes, small improvements, and non-user-facing changes
Code quality gates
Every PR must pass before merge:
| Check | Tool |
|---|---|
| Lint | ESLint |
| Format | Prettier |
| Type check | TypeScript |
| Tests | Vitest |
| Build | Next.js |
pnpm type-check && pnpm lint && pnpm test:run && pnpm build
Checklist before merge
- Preview environment tested — Regression passes against
-predatabase - No console.log statements
- Database migration is backward compatible — If applicable
- Feature flag in place — If risky
Related standards
standards/core/sdlc.md-- Development lifecyclestandards/reference/tech-stack.md-- Technology choices
Last updated: March 2026