Skip to content
Overview

Troubleshooting

Symptom-indexed fixes grouped by subsystem. Expand an entry for the cause, a specific fix, and a link to deeper docs. Start with pnpm setup:doctor, or use Cmd-F to match what you're seeing.

Last updated on

13 min read

Start here

When you're stuck, run pnpm setup:doctor first. Most of the friction in this kit shows up there as a one-line disabled. Missing X, Y hint.

Jump To A Symptom

Reading The Setup Doctor

Three line shapes show up in the doctor's output:

$ pnpm setup:doctor
  • Stripe billingOK
  • GitHub OAuthdisabledMissing GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
  • Email (log)disabled intentionallySet EMAIL_DELIVERY_MODE=plunk and PLUNK_API_KEY

OK means the integration is fully configured. disabled. Missing X, Y means at least one required env var is empty; fix the listed ones and re-run. disabled intentionally means the kit is gracefully running in a no-config mode (e.g. log email outbox); set the suggested env vars only when you're ready to upgrade.

See also. Setup for the per-integration walkthroughs.

First-Run Setup

BETTER_AUTH_SECRET still uses the starter placeholder, or must be at least 32 characters long

The doctor rejects the placeholder string and any value shorter than 32 characters. Better Auth uses this secret to encrypt session cookies; a weak value would break sessions across deploys.

Fix. Generate one and paste it into apps/web/.env:

openssl rand -base64 32

See also. Quickstart.

Tables don't exist, or Prisma P2021 on first boot

Migrations haven't been applied yet, so Prisma is querying tables Postgres doesn't know about.

Fix. Run pnpm db:migrate:dev once. This creates the schema and generates the Prisma client.

See also. Database.

pnpm version mismatch or corepack errors

The kit pins a specific pnpm version via packageManager in package.json. On Node 22+, corepack activates it automatically; older Node versions miss it.

Fix. Use Node 22+ (check .nvmrc), then enable corepack. Activate the exact value from the packageManager field in package.json (for example pnpm@10.x.x), not @latest:

corepack enable
corepack prepare pnpm@10.x.x --activate

Authentication And OAuth

redirect_uri_mismatch from GitHub or Google

By default, the kit appends /api/auth/callback/<provider> to NEXT_PUBLIC_APP_URL. A scheme, hostname, port, path, or trailing-slash mismatch between that URL and the provider console causes this error. BETTER_AUTH_URL and the kit's OAuth proxy are advanced exceptions; see Authentication: Configuring OAuth Providers before changing the callback base.

Fix. GitHub OAuth Apps allow one Authorization callback URL. For direct setup, use separate development and production apps with http://localhost:3000/api/auth/callback/github and https://example.com/api/auth/callback/github. Google Web application clients allow multiple entries, so one client can contain both http://localhost:3000/api/auth/callback/google and https://example.com/api/auth/callback/google, plus both matching authorized JavaScript origins.

See also. Setup: GitHub OAuth and Setup: Google OAuth.

OAuth button rendered greyed-out or disabled

Only one half of the provider's env-var pair is set. The kit hides providers it can't fully wire up: setting GITHUB_CLIENT_ID without GITHUB_CLIENT_SECRET (or vice versa) leaves the button visible but disabled rather than registering a broken provider.

Fix. Run pnpm setup:doctor and look for the provider's line. It tells you which env name is missing.

Sign-in works locally but fails on the deployed site

NEXT_PUBLIC_APP_URL doesn't match the deployed origin. The kit constructs the OAuth callback URL from this var; if your prod app boots with NEXT_PUBLIC_APP_URL=http://localhost:3000, the callback Better Auth advertises won't match what's registered with the provider.

Fix. Set NEXT_PUBLIC_APP_URL to the production origin in your host's environment (Vercel project settings, Fly secrets, Render env, Docker env). It's a NEXT_PUBLIC_* var, so it requires a rebuild on every host.

See also. Deployment: Build-Time vs Runtime Env.

Security Headers And CSP

My third-party script (Sentry, Hotjar, Intercom, etc.) is blocked by CSP

Nosecone applies a strict default Content Security Policy plus a curated allow-list (Turnstile, S3, and Scalar via cdn.jsdelivr.net). Anything not on the list, including new analytics or chat widgets you add, gets blocked at the browser. PostHog is meant to ride the same-origin /ingest proxy, so CSP does not need posthog.com. The browser console shows Refused to load the script ... because it violates the following Content Security Policy directive.

Fix. Extend the allow-list in apps/web/lib/security-headers.ts. Each third-party usually needs entries on script-src, connect-src, and sometimes img-src and frame-src. Test in dev (with the same CSP) before deploying.

See also. Security: Edge: Headers, CSP, CORS.

Turnstile widget doesn't render or token validation fails

Two common causes. First: only one of NEXT_PUBLIC_TURNSTILE_SITE_KEY or TURNSTILE_SECRET_KEY is set; the kit needs both. Second: the widget's domain list in the Cloudflare dashboard doesn't include localhost (for dev) or the production hostname.

Fix. Run pnpm setup:doctor to check both env vars are set. In the Cloudflare dashboard, edit the widget and add localhost (development) and your production domain.

See also. Setup: Cloudflare Turnstile.

Uploads, contact form, or auth emails return SERVICE_UNAVAILABLE

The abuse policy is failing closed. Protected surfaces (uploads, AI chat, transactional email, contact) refuse the request when Upstash is unreachable or the policy cannot decide. In production, missing either UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is a boot failure (assertValidSetupEnv), not a runtime toast. In non-production, missing config is auto-bypassed (a one-time dev_no_upstash warning in the server log).

Fix. Run pnpm setup:doctor and check the Abuse protection line. Set both Upstash env vars from your Upstash database's REST API panel. In production, both are required before the app will start.

See also. Security: Abuse Protection (Upstash).

Server actions break on the second request after a multi-instance deploy

Next.js encrypts server-action payloads with a per-process key by default. With more than one replica, request A goes to replica 1 (gets a payload encrypted with key 1), follow-up request B goes to replica 2 (which can't decrypt with its own key 2). Symptom: server actions work the first time and then fail with a decryption error.

Fix. Generate a 32+ byte secret with openssl rand -base64 32 and set it as NEXT_SERVER_ACTIONS_ENCRYPTION_KEY on every replica. Same value across all instances.

See also. Security: Operational Secrets.

Storage And Uploads

"Failed to upload to S3" toast with no other info

Almost always missing CORS on the bucket. The browser PUTs directly to a presigned URL; without AllowedMethods: ["PUT"] from your app origin, the PUT is silently rejected by the storage provider before the kit can see anything.

Fix. Apply this CORS rule to the bucket:

[
  {
    "AllowedOrigins": ["https://your-app.com"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"]
  }
]

Add http://localhost:3000 for local development.

See also. Storage: Required Bucket Configuration.

Images upload successfully but never display

NEXT_PUBLIC_S3_PUBLIC_URL doesn't match where uploaded objects are publicly served. Cloudflare R2 is the common culprit: the endpoint URL (https://<account-id>.r2.cloudflarestorage.com) is for API calls, not public reads. Public reads go through https://pub-<bucket-id>.r2.dev or your custom domain.

Fix. Open one of the uploaded objects directly in your browser via the URL the bucket generates; that's your NEXT_PUBLIC_S3_PUBLIC_URL (minus the object key). Update .env and rebuild.

See also. Setup: Storage.

Avatars and logos work, but chat attachments stay disabled

The public bucket covers avatars and organization logos only. Chat attachments need a separate private bucket.

Fix. Create a private bucket with public access blocked, then set:

S3_BUCKET_NAME_ATTACHMENTS="your-private-attachments-bucket"

Restart after editing .env.

See also. Setup: Storage.

tmp/ keeps growing in the bucket

A user abandoned an upload between presign and finalize. The kit cleans up tmp/ keys when finalize succeeds, fails, or errors mid-way; the one case it can't observe is a user who walked away with the presigned URL still valid.

Fix. Add a bucket lifecycle rule that expires tmp/ after 1 day:

{
  "Rules": [
    {
      "ID": "expire-tmp-uploads",
      "Status": "Enabled",
      "Filter": { "Prefix": "tmp/" },
      "Expiration": { "Days": 1 }
    }
  ]
}

See also. Storage: Required Bucket Configuration.

Billing And Webhooks

Webhook signature verification failed, or 400 on every Stripe event

Wrong signing secret. The signing secret is per-endpoint and per-Stripe-CLI-session: the production endpoint's whsec_* is different from the one the Stripe CLI prints when you run stripe listen.

Fix. Match the secret to the endpoint sending events. For local dev, stripe listen --forward-to localhost:3000/api/webhooks/stripe prints a fresh whsec_* each session; paste that into STRIPE_WEBHOOK_SECRET in .env. For production, copy the secret from the Stripe Dashboard's webhook detail page.

See also. Webhooks And Async Workflows: Endpoint And Signature Verification.

Checkout completes but the dashboard still shows the free plan

The Subscription row is created from the customer.subscription.created webhook, not from the success redirect. If the webhook didn't reach the kit (firewall, wrong URL, signature mismatch), the dashboard reads no subscription and falls back to the free phase.

Fix. Check the Stripe Dashboard's webhook delivery log for the offending event. A red entry means Stripe couldn't reach you or got a non-2xx; click into it to see the response body. Re-deliver after fixing.

See also. Billing: How A Subscription Comes To Life.

Dashboard shows "Billing Attention Needed", or Pro/Ultra features stay blocked

A live subscription's price.id isn't in the kit's billing catalog (configuration_error phase). Either you created a new Price in Stripe but didn't mirror it as STRIPE_PRICE_ID_PRO_* / STRIPE_PRICE_ID_ULTRA_*, or you bumped the env var but didn't restart (these are server-only env vars).

Fix. Ensure the monthly and yearly Price IDs for Pro and Ultra match Stripe, then restart. The configuration_error phase is intentional: the kit refuses to grant paid features when it can't map a subscription back to a plan. The dashboard surfaces this as a "Billing Attention Needed" alert.

See also. Billing: Subscription Phases.

Email

Why aren't my emails being sent?

EMAIL_DELIVERY_MODE auto-resolved to log (the default in dev, no setup required) or noop (the default when NODE_ENV=test). Both succeed silently without sending real email, which is correct behavior outside production.

Fix. For local inspection, open .local/email-outbox/ in your file browser. To enable real delivery, set PLUNK_API_KEY and EMAIL_FROM (an address or domain verified with Plunk); the mode auto-switches to plunk. pnpm setup:doctor reports the active mode on the Email line.

See also. Email: Three Delivery Modes and Setup: Email.

Plunk emails go to spam or bounce

The sender domain isn't authenticated. SPF, DKIM, and DMARC DNS records establish your domain's authority to send mail; without them, Gmail and Outlook deliverability drops sharply or rejects entirely.

Fix. In the Plunk dashboard, add and verify your sending domain. Plunk walks you through the three DNS records. After verification, send a test email and confirm it lands in the inbox.

See also. Going To Production: Email Sender Configuration.

AI

AI chat fails immediately with an authentication error from the gateway

AI_GATEWAY_API_KEY is missing or invalid. The key is not in apps/web/.env.example, so a fresh clone won't have it; you have to add it manually. pnpm setup:doctor doesn't track this var either.

Fix. Add the key to apps/web/.env:

AI_GATEWAY_API_KEY="vck_..."

Get the value from vercel.com/dashboard/ai-gateway. Restart the dev server after editing .env.

See also. Setup: AI.

"AI chat is temporarily unavailable" toast for a signed-in user

The abuse policy is failing closed for chat. In production this usually means Upstash is unreachable at runtime, or the request context is missing a required characteristic (userId or organizationId). Missing Upstash env vars in production prevent boot entirely (assertValidSetupEnv); they do not surface as this toast. In non-production, missing_config is auto-bypassed (you'll see a dev_no_upstash warning in the server log), so this toast in dev almost always points to a missing characteristic.

Fix. Confirm both Upstash env vars are set and Redis is reachable. Run pnpm setup:doctor and check the Abuse protection line. In dev, unset both vars to use the auto-bypass, or set both from your Upstash REST API panel.

See also. Security: Abuse Protection (Upstash).

Image attachments fail when web search is on

This is documented behavior, not a bug. Web search routes the chat through Perplexity Sonar, which doesn't accept image inputs.

Fix. Disable web search before attaching images, or remove the attachments before enabling web search.

See also. AI: What's Wired In.

Analytics And Monitoring

Stack traces in PostHog are still minified after configuring POSTHOG_API_KEY

POSTHOG_API_KEY and POSTHOG_PROJECT_ID are read at build time, not runtime. The kit's withPostHogConfig wrapper uploads source maps during next build; if those env vars aren't present at build time, the upload silently no-ops.

Fix. Set them as build-time env vars on your host. Vercel: Environment Variables panel (they're available at build by default). Fly: [build.args] in fly.toml. Render: envVars in render.yaml. Docker: --build-arg.

See also. Monitoring: Source Maps.

PostHog events don't arrive even though setup:doctor says PostHog analytics: OK

The client sends events to same-origin /ingest. When POSTHOG_PROXY_INGEST_HOST and POSTHOG_PROXY_ASSET_HOST are unset, those rewrites no-op, so /ingest never reaches PostHog—even though the doctor reports analytics OK. Ad-blockers can also strip requests if traffic falls back to public PostHog hosts.

Fix. Set both proxy hosts in your environment so next.config.ts rewrites /ingest/* to PostHog. Restart or rebuild after editing. Same-origin ingest also avoids most ad-blocker interference.

See also. Monitoring: The Reverse Proxy.

PostHog UI shows no events or wrong project

Wrong region. Leave the host overrides blank for PostHog US Cloud defaults. EU Cloud projects need EU host URLs.

Fix. For EU Cloud, set:

NEXT_PUBLIC_POSTHOG_HOST="https://eu.i.posthog.com"
NEXT_PUBLIC_POSTHOG_UI_HOST="https://eu.posthog.com"

For US Cloud, leave both blank (or use the US defaults from Setup). Restart and rebuild after editing.

See also. Setup: Analytics (PostHog).

Deployment And Self-Hosting

Changing a NEXT_PUBLIC_* env var doesn't take effect

Every NEXT_PUBLIC_* value is inlined into the browser JS bundle at build time. Editing the var on a running server changes nothing; the bundle the browser downloads still contains the old value.

Fix. Trigger a rebuild on every host that reads it. Vercel: redeploy. Fly: fly deploy with --build-arg mirroring the new value. Render: trigger a deploy. Docker: rebuild the image with --build-arg.

See also. Deployment: Build-Time vs Runtime Env.

Migrator container fails on Docker self-host with "could not connect to server"

DATABASE_URL points at localhost or another host the migrator/web containers cannot reach. The Compose file ships migrator, web, and docs only—buyers bring their own Postgres; there is no bundled db service.

Fix. Point DATABASE_URL at a Postgres instance reachable from inside the containers (managed Postgres hostname, or host.docker.internal / host networking when the database runs on the Docker host). Then run:

docker compose run --rm migrator

See also. Deployment: Self-Hosted Docker.

pnpm setup:doctor runs locally, but my deployment uses different env vars

The doctor reads apps/web/.env and validates that file. Production env vars come from your host's environment (Vercel project settings, Fly secrets, Render env, Docker env), which the doctor can't reach.

Fix. Mirror the same env vars in your host's environment, then re-deploy. See Going To Production: Operational Environment Variables and the Environment Variables reference for what needs to land in production.

Where To Go Next

Also useful: Going To Production and Quickstart.

Was this page helpful?

On this page