Preview Environment Testing
What it is
Every PR that targets main gets its own isolated preview environment — a Vercel preview deployment backed by its own Neon database branch. The database is a copy-on-write fork of production: it starts with all production tables and data (currently just the bootstrap state, since we're pre-launch), plus an additive test organization inserted by the preview seed.
Preview environments let you test schema migrations, new features, and UI changes without touching production or the demo environment.
Lifecycle
When a preview environment is created
Push to PR branch
|
v
Vercel detects preview deployment
|
v
Neon auto-creates a copy-on-write DB branch from production
(branch name: preview/<git-branch-name>)
|
v
Neon injects branch-specific DATABASE_URL into the deployment
(this overrides the generic Preview-scoped env var for this build)
|
v
vercel-build.sh runs:
1. Log DB host (safety check — aborts if it resolves to production)
2. CREATE EXTENSION IF NOT EXISTS vector (pgvector for org-intel embeddings)
3. drizzle-kit migrate (applies any new migration files from the PR)
4. seed.ts --preview (inserts Preview Test Org — additive, no truncate)
5. pnpm build (builds Next.js)
|
v
Preview URL is live (e.g. givelink-abc123-datawake-vb.vercel.app)
Safety guard: If the DB host resolves to ep-restless-pond (production) — meaning preview branching isn't firing — the build script prints a loud !!! WARNING and skips migrations/seed rather than mutating production. The Next.js build still runs, but the app will use production data. See the troubleshooting entry "Preview connects to production database" for the fix.
When it's torn down
When a PR is closed or merged, the Neon integration deletes the preview database branch. The Vercel preview deployment stays accessible for a while but the database connection will fail. No manual cleanup needed.
How it differs from demo and production
| Production | Demo | Preview | |
|---|---|---|---|
| URL | givelink.ai / app.givelink.ai | demo.givelink.ai | givelink-{hash}-datawake-vb.vercel.app |
| Database | Neon main branch (ep-restless-pond) | Neon demo branch (ep-silent-recipe) | Neon auto-branch (copy-on-write from production) |
| Auth | Clerk (production instance) | Bypassed (DEV_AUTH_BYPASS=true) | Clerk (dev/preview instance) |
| Data | Real (empty pre-launch) | Hope Foundation canonical dataset | Copy of production + Preview Test Org |
| Schema sync | Manual db:migrate before deploy | Auto via sync-demo-schema.yml (db:push) | Auto via vercel-build.sh (drizzle-kit migrate) |
| Persistence | Permanent | Permanent (long-lived branch) | Ephemeral (deleted on PR close) |
| Who can access | Anyone (public pages) / Clerk users (dashboard) | Anyone (fully public, no auth) | Vercel team members (SSO is off but URL is unguessable) |
What data is available
Preview databases start as a copy-on-write snapshot of production at the moment the branch is created. Pre-launch, production is essentially empty (just the bootstrap schema). On top of that, the preview seed adds:
| Entity | Details |
|---|---|
| Organization | "Preview Test Org" (org_preview_test), slug preview-test, plan tier starter, status active |
| User | "Preview Tester" (user_preview_test), email preview@test.givelink.ai, Clerk ID clerk_preview_test |
| Org membership | The test user is owner of Preview Test Org |
| Campaign | "Preview Test Campaign", donation type, active, $10,000 goal, suggested amounts $25/$50/$100 |
The seed is idempotent — if you re-trigger a deploy (push another commit), it won't duplicate the test org.
Post-launch behavior
Once production has real data, preview branches will inherit that data via copy-on-write. This is powerful for testing — you get realistic data without any setup. When PII becomes a concern, we'll create a preview-base Neon branch with sanitized data as the copy-on-write parent (documented in the spec, not yet implemented).
How to authenticate
Preview deployments use the Clerk dev/preview instance (the Preview-scoped CLERK_SECRET_KEY and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY). This is a separate Clerk application from production.
Option A: Sign in via Clerk dev instance
- Open the preview URL
- Navigate to
/sign-in(or click any dashboard link — you'll be redirected) - Sign in with test credentials from the Clerk dev dashboard
- Select or create an organization in Clerk
This is the most realistic test path — it exercises the full Clerk flow including org selection, JIT provisioning, and role assignment.
Option B: Use SKIP_ENV_VALIDATION for build-only testing
The Preview scope has SKIP_ENV_VALIDATION=1 set. This means the app boots even if some env vars are misconfigured. It doesn't bypass auth at runtime — you still need to sign in. It just prevents build failures from env validation.
No DEV_AUTH_BYPASS on preview
DEV_AUTH_BYPASS is scoped only to demo/showcase, not generic Preview. Preview deployments always require Clerk sign-in. This is intentional — preview is for testing the real auth flow, demo is for unauthenticated sharing.
Testing scenarios
New visitor (no account)
What to test: Public pages, marketing site, donate flow (portal).
- Open the preview URL — you should land on the public marketing pages
- Navigate to
/donate/preview-test-campaign(the seeded campaign's public donation page) - Verify the campaign loads with correct details ($10,000 goal, suggested amounts)
- The donation form should render even without signing in
Edge case: Try navigating to /campaigns or other dashboard routes — you should be redirected to sign-in, not get a 500.
Logged-in user with seeded data
What to test: Dashboard, campaigns, contacts, settings.
- Sign in via Clerk dev instance
- If the preview seed ran correctly, you can access Preview Test Org's dashboard
- Verify the seeded campaign appears in the campaigns list
- Create a new campaign, contact, or event — it should persist within this preview branch only
Stripe Connect (onboarded vs not-onboarded)
What to test: Payment processing, Stripe onboarding flow, fee calculations.
- Preview deployments use the Preview-scoped
STRIPE_SECRET_KEY(Stripe test mode) - The seeded Preview Test Org starts not onboarded to Stripe Connect
- To test the onboarding flow: navigate to Settings > Payments and start the Stripe Connect Express onboarding
- Use Stripe test card numbers:
4242 4242 4242 4242for successful payments - To test an already-onboarded org: complete the Stripe onboarding in the preview environment (it persists for the life of the PR)
Different access levels (roles)
What to test: Permission gating for owner, admin, editor, viewer.
GiveLink has four org roles: owner, admin, editor, viewer. The preview seed creates one user as owner. To test other roles:
- Create additional test users in the Clerk dev dashboard
- Sign in as each user and join the Preview Test Org
- Assign different roles via the org settings (as the owner)
- Verify permission boundaries: editors can't access billing, viewers can't edit campaigns, etc.
Plan tiers (feature gating)
What to test: Feature gating between starter, core, and pro tiers.
The preview seed creates the org on the starter tier. To test other tiers, update the org's planTier directly in the database:
-- Connect to the preview branch via Neon Console > Branches > [preview branch] > SQL Editor
UPDATE organizations SET plan_tier = 'pro' WHERE id = 'org_preview_test';Verify that pro-only features (AI agents, advanced reporting, custom branding) appear/disappear based on tier.
Multi-tenant isolation
What to test: Org A can't see Org B's data (RLS enforcement).
- Create a second organization in the preview environment via the dashboard
- Add a campaign and contacts to each org
- Switch between orgs — verify campaigns/contacts from the other org are invisible
- Check API responses (browser devtools > Network) to confirm no cross-org data leakage
AI agents
What to test: Agent configuration, suggestion mode, auto mode.
- Navigate to the AI Agents section in the dashboard
- Agents use the Vercel AI Gateway — the Preview-scoped API keys should work
- Test
Offmode (no suggestions),Suggestmode (shows suggestions without acting), andAutomode (acts autonomously) - Verify agent usage tracking records token counts and costs
Email flows
What to test: Donation receipts, email sequences, digest emails.
- Preview deployments use the Preview-scoped
RESEND_API_KEY(Resend test mode) - Resend in test mode logs emails but doesn't deliver to real inboxes
- Check the Resend dashboard (resend.com) for sent emails after triggering:
- A donation (receipt email)
- An email sequence enrollment
- A manual email from the contacts page
Salesforce integration
What to test: Connected vs disconnected Salesforce state.
- The preview environment starts with no Salesforce connection (the preview seed doesn't configure one)
- To test the connected flow, you'd need a Salesforce sandbox org connected via OAuth
- For disconnected state: verify the Salesforce settings page shows the connection prompt, and that sync-dependent features degrade gracefully (no crashes, clear "not connected" messaging)
Edge cases
| Scenario | How to test |
|---|---|
| Empty org (no campaigns/contacts) | Create a new org in the preview, don't add any data. Verify dashboard shows empty states, not errors. |
| Lapsed donors | If post-launch with real data, look for contacts with old last-donation dates. Pre-launch, manually insert a contact with a lastDonationAt from 2 years ago. |
| Refunded payments | Process a test payment via Stripe, then refund it from the Stripe dashboard. Verify the refund syncs back. |
| Webhook replay | Use the Stripe CLI (stripe trigger payment_intent.succeeded) to replay webhook events against the preview URL's /api/webhooks/stripe endpoint. |
How to reset or re-seed
Light reset (re-run the additive seed)
The preview seed is idempotent — if the test org already exists, it skips. To force a re-seed:
- Delete the test org from the preview database (via Neon SQL Editor on the branch)
- Push a new commit to the PR branch (or re-run the Vercel deployment)
- The build will re-run
seed.ts --previewand re-create the test org
Full reset (start with a fresh copy-on-write)
If the preview data is too messy to salvage:
- Delete the Neon preview branch manually (Neon Console > Branches)
- Push a new commit to the PR branch
- Neon creates a fresh copy-on-write branch from production
- The build applies migrations and seeds from scratch
Nuclear option (for stuck deploys)
If the Vercel deployment is stuck or the Neon branch is in a bad state:
# Force a fresh Vercel deployment
vercel deploy --yesEnvironment variables on preview deployments
Preview deployments inherit env vars from two sources.
Injected by Neon per-deployment (branch-specific)
| Variable | Value |
|---|---|
DATABASE_URL | Pooled connection to the preview Neon branch |
DATABASE_URL_UNPOOLED | Direct connection to the preview Neon branch |
PGHOST | Preview branch hostname |
POSTGRES_* | Various connection formats for the preview branch |
These override the generic Preview-scoped values. Each deployment gets its own unique database.
Inherited from generic Preview scope (shared across all previews)
| Variable | Purpose |
|---|---|
CLERK_SECRET_KEY | Clerk dev/preview instance |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Clerk client-side key |
STRIPE_SECRET_KEY | Stripe test mode |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe client-side test key |
STRIPE_WEBHOOK_SECRET | Webhook signature verification |
RESEND_API_KEY | Resend test mode |
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY | Inngest dev server |
SKIP_ENV_VALIDATION | 1 — prevents build failures from missing optional vars |
NEXT_PUBLIC_APP_DOMAIN | app.givelink.ai |
FIRECRAWL_API_KEY | For org intelligence features |
Not available on preview (scoped to demo/showcase only)
| Variable | Why |
|---|---|
DEV_AUTH_BYPASS | Auth bypass is demo-only — preview tests the real Clerk flow |
DEV_BYPASS_ORG_ID | Only meaningful with auth bypass |
Troubleshooting
"Build fails with migration error"
Cause: The PR branch has schema changes but is missing the corresponding migration file in drizzle/.
Fix: On the PR branch, run:
pnpm db:generateThis creates a numbered migration SQL file. Commit it and push — the preview build will pick it up.
"Build fails with relation already exists"
Cause: A migration file tries to CREATE a table that already exists. This usually means the branch is behind main and has stale migration files, or a migration was partially applied.
Fix: Rebase the PR branch onto main:
git fetch origin
git rebase origin/main
git push --force-with-lease"Preview URL returns 500 on all routes"
Cause: Missing env vars. Check the Vercel build logs for env validation failed or CLERK_SECRET_KEY is required.
Fix: Verify the generic Preview-scoped secrets are set:
vercel env ls | grep PreviewThe 7 required secrets are: CLERK_SECRET_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, RESEND_API_KEY, CRON_SECRET.
"Preview connects to production database"
Symptom: Build logs show DB host: ep-restless-pond-... (the production endpoint) instead of a new preview branch endpoint. The safety guard in vercel-build.sh detects this and skips migrations/seed with a !!! WARNING message to prevent production mutation.
Cause: The Neon-Vercel integration's "Create database branch for deployment" option is not enabled on the project connection. The connection exists but doesn't include a preview deployment action, so Neon never creates a branch and the build inherits the production-pointing DATABASE_URL from the generic Preview scope.
Fix (must be done through Vercel UI — the API deploymentActions field does not persist the preview toggle):
- Go to Vercel Dashboard → givelink project → Storage →
neon-pink-park - Click the Projects tab
- Three-dot menu on the
givelinkrow → Remove Project Connection → confirm - Click Connect Project → select
givelink - Check Development, Preview, and Production under "Environments"
- Under "Create Database Branch For Deployment" check Preview (leave Production unchecked — you don't want a new branch for every production deploy)
- Click Connect
Gotcha: If the Connect form rejects with "already has an existing environment variable with name POSTGRES_URL…" or similar, the demo/showcase-scoped DB env vars are blocking the create (same name, same preview scope, just branch-qualified). Temporarily delete the 8 demo/showcase-scoped DB env vars, run Connect, then re-add them afterward with gitBranch: "demo/showcase".
Verification: After fixing, push a commit to any PR branch. You should see:
- Neon Console → Branches — a new
preview/<branch-name>branch - Vercel build log —
DB host: ep-<something-other-than-restless-pond> - Connection config should include
"deployments": {"actions": [{"environments": ["preview"], "slug": "Neon"}], "required": true}
"The seed says 'Preview test org already exists — skipping'"
Expected behavior. The seed is idempotent. If you need to recreate the test org, delete it from the database first (see How to reset).
"Preview Clerk keys silently point at production"
Symptoms:
- Preview URLs (
*.vercel.apporgivelink-git-<branch>-datawake-vb.vercel.app) fail to render the sign-in page, redirect-loop, or throw Clerkdomain not allowederrors in the browser console. - Served HTML contains
pk_live_...instead ofpk_test_.... - CSP/script-src headers reference
clerk.givelink.aiinstead of*.clerk.accounts.dev. CLERK_SECRET_KEYin the Preview scope is empty, malformed (surrounding"quote chars), or still ask_live_...value.
Cause: The Preview scope was never pointed at the Clerk Development instance. Production Clerk is domain-locked to app.givelink.ai, so its pk_live_ key can't authenticate on any other hostname — every preview URL fails.
Detection:
# Pull the Preview scope into a temp file and inspect the Clerk values
vercel env pull .env.preview-check --environment=preview --yes
grep -E '^(NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY|CLERK_SECRET_KEY)=' .env.preview-check
# Bad: "pk_live_...", empty "", or "\"sk_live_...\""
# Good: "pk_test_...", "sk_test_..."
# Also check the served HTML on a preview URL
curl -sS https://givelink-git-<branch>-datawake-vb.vercel.app/sign-in \
| grep -oE 'pk_(test|live)_[A-Za-z0-9]+' | head -1Fix:
-
Copy the
pk_test_...andsk_test_...from the Clerk Development instance (dashboard.clerk.com → GiveLink app → switch instance dropdown → Development → API Keys). The dev instance uses a shared*.accounts.devdomain that works on any hostname — no domain lock. -
Rotate both scopes. Use the REST API, not the CLI —
vercel env add <name> preview --value <v> --yeshas a known bug where it returnsaction_required: git_branch_requiredeven when--yesis passed and "all Preview branches" is the documented default (verified broken on CLI 51.1.0 and 51.2.1, 2026-04-14):PROJECT=$(jq -r .projectId .vercel/project.json) TEAM=$(jq -r .orgId .vercel/project.json) # Remove the bad values first (CLI rm works fine) vercel env rm NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY preview --yes vercel env rm CLERK_SECRET_KEY preview --yes vercel env rm NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY development --yes vercel env rm CLERK_SECRET_KEY development --yes # Add via REST API (works around the CLI bug) add_env() { curl -sS -X POST "https://api.vercel.com/v10/projects/${PROJECT}/env?teamId=${TEAM}&upsert=true" \ -H "Authorization: Bearer ${VERCEL_TOKEN}" -H "Content-Type: application/json" \ -d "$(jq -n --arg k "$1" --arg v "$2" --arg t "$3" --argjson tgt "$4" \ '{key:$k, value:$v, type:$t, target:$tgt}')" } add_env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 'pk_test_...' plain '["preview"]' add_env CLERK_SECRET_KEY 'sk_test_...' encrypted '["preview"]' add_env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 'pk_test_...' plain '["development"]' add_env CLERK_SECRET_KEY 'sk_test_...' encrypted '["development"]' -
Trigger a redeploy (empty commit + push, or
vercel deploy --yesfrom the branch). -
Verify the served HTML on the new preview URL contains
pk_test_andclerk.accounts.dev, zeropk_live_orclerk.givelink.aireferences.
"Clerk sign-in page shows 'Invalid publishable key'"
Cause: The Preview-scoped NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY doesn't match the Clerk application configured for development/preview.
Fix: Verify the key in Vercel matches the one in your Clerk dashboard (Development instance > API Keys). See the entry above ("Preview Clerk keys silently point at production") for the full rotation procedure.
"Stripe payments fail with 'No such connected account'"
Cause: The preview org hasn't completed Stripe Connect onboarding. Preview branches don't inherit Stripe Connect state from production — each preview starts fresh.
Fix: Complete the Stripe Connect Express onboarding flow in the preview environment's Settings > Payments page. Use Stripe test mode — no real bank account needed.
Cost
| Resource | Cost impact |
|---|---|
| Neon preview branches | Copy-on-write — near zero storage until data diverges. Compute charges only when active. Auto-suspend after 5 min idle. |
| Vercel preview deployments | Included in Pro plan. Builds count toward monthly build minutes. |
| Stripe test mode | Free |
| Clerk dev instance | Free tier |
| Resend test mode | Free |
Estimated added cost for preview environments: $0-5/month pre-launch, scaling with PR activity.
References
scripts/vercel-build.sh— the build script that runs migrations + seed for previewscripts/seed.ts— production guard +--previewadditive modedrizzle.config.ts— usesDATABASE_URL_UNPOOLEDfor migrationssrc/lib/dev-bypass.ts— whyDEV_AUTH_BYPASSdoesn't apply to previewsrc/lib/auth.ts— org ID resolution with bypass fallbacksrc/proxy.ts— Clerk middleware and hostname routing- ADR-0047 — push to generate+migrate switch
- Demo Environment runbook — the long-lived demo at
demo.givelink.ai - Spec:
docs/superpowers/specs/2026-04-13-preview-environment-spec.md - Plan:
docs/superpowers/plans/2026-04-13-preview-environment-migration.md