Setup
Turn on the optional integrations one at a time. Same shape per section so you never get lost.
Quickstart gets the app running with three env vars. This page is where you turn on the optional integrations: OAuth providers, captcha, billing, email, storage, AI, analytics, monitoring, and abuse protection. Every card follows the same shape: a short intro, the steps to do on the provider's side, the env vars to set, and a verification step. Anything you skip stays gracefully degraded in the UI; the kit always boots.
The Setup Doctor
Run pnpm setup:doctor after editing apps/web/.env. It validates the core vars, then prints one line per optional integration showing whether it's enabled, what's missing, and how to fix it.
Core setup
- DATABASE_URLOK
- NEXT_PUBLIC_APP_URLOK
- BETTER_AUTH_SECRETOK
Optional integrations
- Email (log)OK
- GitHub OAuthdisabledMissing GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
- Google OAuthOK
- Turnstile captchaOK
- Billing (Stripe)OK
- Billing provider consistencyOK
- AI chat (Vercel AI Gateway)OK
- PostHog analyticsOK
- Storage uploadsOK
- Contact formOK
- Abuse protectionOK
Use the doctor as your green-light check after each card below: the line you care about should read OK. See Commands And Scripts for what else the script does.
Optional Integrations
Email + password authentication ships enabled by default. Two-factor auth (TOTP + backup codes) and passkeys (WebAuthn) are wired and ready without any env vars; users opt in from /dashboard/account/security. Expand any integration below to turn it on.
GitHub OAuthOptionalLet users sign in with their GitHub account. Email + password keeps working alongside it.doctor:GitHub OAuth: OK
Create the OAuth app
- Open github.com/settings/developers -> OAuth Apps -> New OAuth App.
- Homepage URL: your
NEXT_PUBLIC_APP_URL(e.g.http://localhost:3000). - Authorization callback URL:
<NEXT_PUBLIC_APP_URL>/api/auth/callback/github. - Click Register application, then Generate a new client secret. Copy both the Client ID and Client Secret.
Set the env vars
| Var | Example |
|---|---|
GITHUB_CLIENT_ID | Iv1.abc123... |
GITHUB_CLIENT_SECRET | ghp_xyz... |
Verify
pnpm setup:doctor reports GitHub OAuth: OK. Visit /auth/sign-in and the GitHub button is active (not greyed out).
For deeper coverage (callback wiring, scopes, adding a new provider), see Authentication.
Google OAuthOptionalLet users sign in with their Google account. Same shape as GitHub; the provider console is different.doctor:Google OAuth: OK
Create the OAuth client
- Open console.cloud.google.com/apis/credentials -> Create Credentials -> OAuth client ID.
- Application type: Web application.
- Authorized JavaScript origins: your
NEXT_PUBLIC_APP_URL. - Authorized redirect URIs:
<NEXT_PUBLIC_APP_URL>/api/auth/callback/google. - Click Create. Copy the Client ID and Client Secret.
Set the env vars
| Var | Example |
|---|---|
GOOGLE_CLIENT_ID | 1234567890-abc.apps.googleusercontent.com |
GOOGLE_CLIENT_SECRET | GOCSPX-... |
Verify
pnpm setup:doctor reports Google OAuth: OK. The Google button is active on /auth/sign-in.
For deeper coverage, see Authentication.
Cloudflare TurnstileOptionalCaptcha protection on sign-up, sign-in, password reset, and the public contact form. Free for any traffic volume.doctor:Turnstile captcha: OK
Create the site widget
- Open dash.cloudflare.com -> Turnstile -> Add site.
- Widget mode: Managed (recommended).
- Domains: your production hostname plus
localhostfor local dev. - Copy the Site Key (public) and Secret Key (server-only).
Set the env vars
| Var | Example |
|---|---|
NEXT_PUBLIC_TURNSTILE_SITE_KEY | 0x4AAAAAAA... |
TURNSTILE_SECRET_KEY | 0x4AAAAAAA... |
Verify
pnpm setup:doctor reports Turnstile captcha: OK. Open /auth/sign-up and the captcha widget renders below the form.
For deeper coverage, see Security: Authentication, Sessions, And Captcha.
Billing (Stripe)OptionalSubscriptions, the customer portal, the dashboard pricing grid, and lifecycle emails. Stripe-hosted checkout; B2B / org-scoped. The default provider when BILLING_PROVIDER is unset.doctor:Billing (Stripe): OK
Billing runs through exactly one provider per deployment. Stripe is the default (BILLING_PROVIDER unset or stripe); the next cards cover Polar and Creem. Only the active provider's variables are required, and switching providers later does not migrate subscriptions, so decide before you have paying customers.
Create the Stripe products
- Sign up at stripe.com; stay in test mode for development.
- Create Products called "Pro" and "Ultra". Add monthly and yearly recurring Prices to each. Copy all four price ids.
- In Developers -> API keys, copy your Secret key (
sk_test_*).
Register the webhook endpoint
In the Stripe Dashboard -> Developers -> Webhooks -> Add endpoint:
- URL:
<NEXT_PUBLIC_APP_URL>/api/webhooks/stripe - Events:
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,customer.subscription.trial_will_end,invoice.finalized,invoice.payment_action_required,invoice.payment_failed,invoice.payment_succeeded,charge.refunded,charge.dispute.created.
Copy the signing secret (whsec_*).
For local dev, install the Stripe CLI and run stripe listen --forward-to localhost:3000/api/webhooks/stripe. The CLI prints a whsec_* secret to use locally.
Set the env vars
| Var | Example |
|---|---|
STRIPE_SECRET_KEY | sk_test_51M... |
STRIPE_WEBHOOK_SECRET | whsec_... |
STRIPE_PRICE_ID_PRO_MONTHLY | price_1MxAbc... |
STRIPE_PRICE_ID_PRO_YEARLY | price_1MxDef... |
STRIPE_PRICE_ID_ULTRA_MONTHLY | price_1MxGhi... |
STRIPE_PRICE_ID_ULTRA_YEARLY | price_1MxJkl... |
STRIPE_PRICE_ID_ENTERPRISE_YEARLY (optional) | price_1MxMno... |
Verify
pnpm setup:doctor reports Billing (Stripe): OK. Visit /dashboard/billing; the plan grid renders the shipped Free, Pro, and Ultra cards with working upgrade buttons.
For deeper coverage (subscription phases, customizing plans, the webhook story), see Billing.
Billing (Polar)OptionalThe same subscriptions, portal, pricing grid, and phases, billed through Polar as merchant of record (Polar issues invoices, collects tax, and sends receipts). Set BILLING_PROVIDER=polar instead of the Stripe block.doctor:Billing (Polar, sandbox): OK
Create the sandbox organization and token
- Sign in at sandbox.polar.sh (a separate environment from production, with its own accounts and data) and create an organization.
- Under Settings -> Developers, create an organization access token. Copy it.
Create one product per plan and interval
Polar models one billing interval per product, so create four products: Pro monthly, Pro yearly, Ultra monthly, Ultra yearly, each with a recurring fixed price matching the catalog's amount and currency. Copy the four product ids (UUIDs). A product-level trial is optional; the kit sets the trial at checkout from the catalog.
Register the webhook endpoint
Under Settings -> Webhooks -> Add endpoint:
- URL:
<NEXT_PUBLIC_APP_URL>/api/webhooks/polar - Format: raw
- Events:
subscription.created,subscription.updated,subscription.active,subscription.canceled,subscription.uncanceled,subscription.past_due,subscription.revoked,checkout.updated,order.paid.
Copy the signing secret; Polar shows it once.
For local dev, Polar must reach your machine: expose port 3000 with a tunnel (for example cloudflared tunnel --url http://localhost:3000 or ngrok http 3000) or use Polar's polar listen CLI, and register the tunnel URL as the endpoint. See Billing: Local Development With Polar.
Set the env vars
| Var | Example |
|---|---|
BILLING_PROVIDER | polar |
POLAR_SERVER | sandbox (use production when live; the default when unset) |
POLAR_ACCESS_TOKEN | polar_oat_... |
POLAR_WEBHOOK_SECRET | whsec_... |
POLAR_PRODUCT_ID_PRO_MONTHLY | a product UUID |
POLAR_PRODUCT_ID_PRO_YEARLY | a product UUID |
POLAR_PRODUCT_ID_ULTRA_MONTHLY | a product UUID |
POLAR_PRODUCT_ID_ULTRA_YEARLY | a product UUID |
POLAR_PRODUCT_ID_ENTERPRISE_YEARLY (optional) | a product UUID |
Leave the STRIPE_* block blank. If it is set anyway, the doctor warns that Stripe is ignored.
Verify
pnpm setup:doctor reports Billing (Polar, sandbox): OK and Billing provider consistency: OK. pnpm billing:check-prices compares the four products against the catalog. Visit /dashboard/billing; the plan grid renders with working upgrade buttons, and the sandbox checkout accepts Polar's test card 4242 4242 4242 4242 (Polar sandbox uses Stripe test-mode card numbers).
For the capability differences (no manual webhook replay, Polar sends receipts and trial reminders itself, one customer per email) and the production cutover, see Billing: Connecting Polar and Going To Production: Pricing And Polar Production.
Billing (Creem)OptionalThe same subscriptions, portal, and catalog, billed through Creem as merchant of record. Set BILLING_PROVIDER=creem. The adapter ships. Seats are not advertised.doctor:Billing (Creem, test): OK
Set BILLING_PROVIDER=creem, CREEM_API_KEY, CREEM_WEBHOOK_SECRET, CREEM_SERVER=test (or prod, the default), and CREEM_PRODUCT_ID_* for products you create in the Creem dashboard. Register the webhook at /api/webhooks/creem. Follow Billing: Connecting Creem. Leave the other providers' blocks blank.
pnpm setup:doctor reports Billing (Creem, test): OK when CREEM_SERVER=test. Switching providers later does not migrate subscriptions.
EmailOptionalTransactional email through one of six providers. The default log mode needs zero setup and writes HTML to .local/email-outbox/.doctor:Email (log): OK
Explicit EMAIL_DELIVERY_MODE wins. If it is unset, the first configured
credential wins, in order: plunk, resend, postmark, brevo, sendgrid,
smtp. If none is set, the mode is noop when NODE_ENV=test, otherwise
log. Two credentials without an explicit mode is a boot error.
Local development (default)
No setup needed. The kit writes every outgoing email as an HTML file to .local/email-outbox/. Open the file in a browser. The filename and comments use a recipient hash, not the address.
Production
- Pick one of Plunk, Resend, Postmark, Brevo, SendGrid, or SMTP.
- Verify the sending domain in that provider's dashboard (SPF, DKIM, and DMARC when you can).
- Create the credential for that mode. SMTP needs
SMTP_HOST,SMTP_USER, andSMTP_PASS.
Set the env vars (production)
| Var | Example |
|---|---|
EMAIL_DELIVERY_MODE | resend (or plunk, postmark, brevo, sendgrid, smtp) |
| Provider credential | RESEND_API_KEY, PLUNK_API_KEY, POSTMARK_SERVER_TOKEN, BREVO_API_KEY, SENDGRID_API_KEY, or SMTP_HOST |
EMAIL_FROM | Name <hello@your-domain.com> |
CONTACT_FORM_TO_EMAIL | hello@your-domain.com (only if the public contact form is enabled) |
Verify
pnpm setup:doctor reports Email (<mode>): OK. Trigger a transactional email (sign up a new user). The contact form additionally requires CONTACT_FORM_TO_EMAIL plus Turnstile.
For deeper coverage (delivery modes, React Email templates, swapping providers), see Email.
StorageOptionalS3-compatible object storage for avatars, organization logos, and chat image attachments. Pick a provider; the env-var skeleton is the same.doctor:Storage uploads: OK
Native AWS: no custom endpoint, region required. Path-style stays off (default).
AWS_REGION="us-east-1"
AWS_ENDPOINT_URL_S3=""
AWS_S3_FORCE_PATH_STYLE="false"
AWS_ACCESS_KEY_ID="<your-access-key-id>"
AWS_SECRET_ACCESS_KEY="<your-secret-access-key>"
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGES="your-bucket-name"
NEXT_PUBLIC_S3_PUBLIC_URL="https://your-bucket.s3.us-east-1.amazonaws.com"
# Second bucket. Private: block all public access.
S3_BUCKET_NAME_ATTACHMENTS="your-private-attachments-bucket"R2 uses an account-scoped endpoint and the magic region auto. Free egress is the usual reason buyers pick it.
AWS_REGION="auto"
AWS_ENDPOINT_URL_S3="https://<account-id>.r2.cloudflarestorage.com"
AWS_S3_FORCE_PATH_STYLE="false"
AWS_ACCESS_KEY_ID="<your-r2-access-key-id>"
AWS_SECRET_ACCESS_KEY="<your-r2-secret-access-key>"
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGES="your-bucket-name"
NEXT_PUBLIC_S3_PUBLIC_URL="https://pub-<bucket-id>.r2.dev"
# Second bucket. Private: block all public access.
S3_BUCKET_NAME_ATTACHMENTS="your-private-attachments-bucket"MinIO and most other self-hosted S3 servers expect path-style URLs. Endpoint is your MinIO host.
AWS_REGION="auto"
AWS_ENDPOINT_URL_S3="https://minio.your-domain.com"
AWS_S3_FORCE_PATH_STYLE="true"
AWS_ACCESS_KEY_ID="<your-minio-access-key>"
AWS_SECRET_ACCESS_KEY="<your-minio-secret-key>"
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGES="your-bucket-name"
NEXT_PUBLIC_S3_PUBLIC_URL="https://minio.your-domain.com/your-bucket-name"
# Second bucket. Private: block all public access.
S3_BUCKET_NAME_ATTACHMENTS="your-private-attachments-bucket"S3_BUCKET_NAME_ATTACHMENTS is a second bucket, private, with block-all-public-access on. Chat image uploads stay off unless that bucket is set.
Don't forget the bucket-side CORS rule (allow PUT from your app origin) and a
lifecycle rule expiring tmp/ after 1 day. Without CORS, every upload fails
silently. Both rules are copy-pasteable in Storage required bucket
configuration.
Verify. pnpm setup:doctor reports Storage uploads: OK. Visit /dashboard/account and upload an avatar; it persists and shows up immediately on the navbar.
For deeper coverage (the presign + finalize pipeline, image validation, what gets stored where), see Storage.
AI (Vercel AI Gateway)OptionalOne key unlocks every supported provider through gateway(modelId). No per-provider keys, no swapping SDKs when a new model lands.doctor:AI chat (Vercel AI Gateway): OK
Create a gateway key
- Sign in at vercel.com.
- Open the AI Gateway page from your dashboard.
- Create a new API key. Copy the value.
Set the env var
| Var | Example |
|---|---|
AI_GATEWAY_API_KEY | vck_... |
Verify
pnpm setup:doctor reports AI chat (Vercel AI Gateway): OK. Then visit /dashboard/ai-chat, send a message, and confirm tokens stream back. If the request still fails at the SDK boundary with an authentication error, restart the dev server after editing .env.
For deeper coverage (the streaming chat handler, the ai-elements building blocks, billing gates per model), see AI.
Analytics (PostHog)OptionalProduct analytics, web analytics, session replay, and the foundation that Monitoring builds on.doctor:PostHog analytics: OK
Create a PostHog project
- Sign up at posthog.com.
- Create a project; copy the Project API Key from Project Settings.
- Note your API host (e.g.
https://us.i.posthog.com) and UI host (e.g.https://us.posthog.com). The exact values depend on which region you picked at signup; PostHog shows them on the same settings page.
Set the env vars
| Var | Example |
|---|---|
NEXT_PUBLIC_POSTHOG_KEY | phc_... |
NEXT_PUBLIC_POSTHOG_HOST | https://us.i.posthog.com |
NEXT_PUBLIC_POSTHOG_UI_HOST | https://us.posthog.com |
POSTHOG_PROXY_INGEST_HOST | optional; same as NEXT_PUBLIC_POSTHOG_HOST to enable the same-origin reverse proxy |
POSTHOG_PROXY_ASSET_HOST | optional; pair with POSTHOG_PROXY_INGEST_HOST |
Verify
pnpm setup:doctor reports PostHog analytics: OK. Trigger an action in the app; watch it appear in PostHog -> Live Events within seconds.
For deeper coverage (typed events, identification, session replay, the reverse-proxy story), see Analytics.
Monitoring (PostHog Source Maps and Logs)OptionalBuilds on Analytics. Adds de-minified stack traces in PostHog's Errors view and OpenTelemetry-shaped server logs.
Prerequisite: Analytics enabled. The two env vars below are read at build
time only, so changing them requires a fresh build on every host. Set them
in your build environment (Vercel, Fly, Render, Docker), not just .env.
Create a personal API key
- In PostHog -> Settings -> Personal API Keys -> Create personal API key.
- Scope: Project: write. Copy the key (
phx_...). - Note your numeric Project ID from Project Settings (e.g.
12345).
Set the env vars
| Var | Example |
|---|---|
POSTHOG_API_KEY | phx_... |
POSTHOG_PROJECT_ID | 12345 |
Verify
Run pnpm build. The build log includes a "Source maps uploaded" line from withPostHogConfig. Trigger an unhandled error in the app; PostHog -> Errors shows the issue with a de-minified stack trace.
For deeper coverage (the three pipelines, structured logging via OTLP, the reverse proxy), see Monitoring.
Abuse Protection (Upstash Redis)Required for productionSliding-window rate limits on auth flows, the contact form, AI chat, and uploads. Required before going to production.doctor:Abuse protection: OK
Create an Upstash Redis database
- Sign up at upstash.com.
- Create a Redis database. Any region is fine; the kit uses the REST API so latency is bounded by HTTPS, not Redis protocol.
- Open the database -> REST API panel. Copy the REST URL and REST Token.
Set the env vars
| Var | Example |
|---|---|
UPSTASH_REDIS_REST_URL | https://us1-active-xxx.upstash.io |
UPSTASH_REDIS_REST_TOKEN | AX... |
Verify
pnpm setup:doctor reports Abuse protection: OK. Send 10 sign-in requests in a row from the same IP; the eleventh returns Too Many Requests.
For deeper coverage (per-surface limits, fail-open vs fail-closed policy, Redis tuning), see Security: Abuse Protection (Upstash).