Skip to content
Docs just relaunched - explore the new sidebar, OG images, and AI-ready content.
Overview

Troubleshooting

Symptom-indexed fixes for the friction surfaces that come up most often during setup, deployment, and operation.

Last updated on

10 min read

The entries below are grouped by subsystem. Each one is phrased as a symptom; expand it for the cause, a specific fix, and a link to the deeper docs. Use Cmd-F or the quick-jump cards to find a phrase that matches what you're seeing.

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.

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. Add ...
  • 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:

corepack enable
corepack prepare pnpm@latest --activate

Authentication & OAuth

redirect_uri_mismatch from GitHub or Google

The OAuth app's authorized callback URL doesn't match what the kit constructs at runtime: <NEXT_PUBLIC_APP_URL>/api/auth/callback/<provider>. Common causes: trailing slash, http vs https mismatch, the production URL is registered but you're hitting localhost.

Fix. In the provider's developer console, set the callback URL to exactly your NEXT_PUBLIC_APP_URL plus /api/auth/callback/github or /api/auth/callback/google. Multiple URLs are fine; add both http://localhost:3000/... and your production URL.

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 & 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, PostHog). Anything not on the list, including new analytics or chat widgets you add, gets blocked at the browser. 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.

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 & 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.

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 & 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.

Pro features blocked with "Billing has a configuration error"

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

Fix. Ensure STRIPE_PRICE_ID_PRO_MONTHLY and STRIPE_PRICE_ID_PRO_YEARLY match the Price IDs in Stripe, and restart. The configuration_error phase is intentional: the kit refuses to grant Pro features when it can't map a subscription back to a plan.

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; the mode auto-switches to plunk. pnpm setup:doctor reports the active mode on the Email line.

See also. Email: Three Delivery Modes.

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. As of today, AI_GATEWAY_API_KEY is not yet in apps/web/.env.example, so a fresh clone won't have it; you have to add it manually. pnpm setup:doctor doesn't currently 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. In production this happens when Upstash is unreachable, partially configured (one of UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN is set, the other isn't), or when the request context is missing a required characteristic (userId or organizationId). In non-production environments 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. Run pnpm setup:doctor and check the Abuse protection line. Set both Upstash env vars to the values from your Upstash database's REST API panel, or unset both to fall back to the dev auto-bypass.

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 & 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

Ad-blockers strip requests to i.posthog.com and similar known telemetry hosts. The kit tracks events client-side, so a blocked browser sees no data.

Fix. Configure the same-origin reverse proxy. Set POSTHOG_PROXY_INGEST_HOST and POSTHOG_PROXY_ASSET_HOST in your environment; the kit's next.config.ts rewrites /ingest/* to PostHog. Ad-blockers can't strip same-origin requests.

See also. Monitoring: The Reverse Proxy.

PostHog UI shows no events or wrong project

Wrong region. EU-region projects need EU host URLs; US-region projects need US host URLs.

Fix. Match the region you picked at PostHog signup:

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

Replace eu with us for US-region projects. Restart and rebuild after editing.

Deployment & 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 is using localhost instead of the Docker network hostname. Inside Docker Compose, services reach each other by service name, not by localhost.

Fix. In your .env for Compose, point DATABASE_URL at the service name:

DATABASE_URL="postgresql://user:password@db:5432/syntaxkit"

db here is the service name from docker-compose.yml, not a hostname.

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. The pre-launch checklist on Going To Production: Operational Environment Variables walks through every variable that needs to land in production.

Where To Go Next

Was this page helpful?

On this page