Unstack Pro Docs

Environment Variables

Complete guide to configuring Unstack Pro environment variables

How many places you configure depends on which edition you run. Pick yours with the switcher below — the choice is remembered across every page in these docs.

There is one configuration surface: .env.local in local development, or your host's environment variables (e.g. Vercel) in production. Better Auth runs inside Next.js and talks to Postgres directly, so there is no second deployment environment to populate.

There are two configuration surfaces:

  1. The Next.js environment — .env.local in local development, or your host's environment variables (e.g. Vercel) in production.
  2. The Convex deployment environment — set with bunx convex env set NAME "value" or from the Convex dashboard → Settings → Environment Variables.

Some variables live in only one of these; a few must be set in both. The .env.example file in the repo is split into the same two sections.

Security Notice: Never commit your .env.local file to version control. Keep your secrets secure!

Quick Setup

Copy the example file and fill in your values:

cp .env.example .env.local

That's the whole setup — every variable below goes in that one file.

Then set the Convex-side variables with the CLI (with bun convex dev running):

bunx convex env set NAME "value"

Where Each Variable Lives

VariablePostgres editionConvex edition
DATABASE_URLRequired — .env.localNot used
NEXT_PUBLIC_CONVEX_URLNot usedRequired — .env.local
NEXT_PUBLIC_CONVEX_SITE_URLNot usedRequired — .env.local
CONVEX_DEPLOYMENTNot usedWritten into .env.local by convex dev
BETTER_AUTH_SECRETRequired — .env.localRequired — Convex deployment
JWKSNot usedRequired — Convex deployment
BETTER_AUTH_URLNot usedConvex deployment; unused at runtime
EMAIL_FROMRequired — .env.localRequired — Convex deployment
RESEND_API_KEYRequired — .env.localRequired — Convex deployment
PASSKEY_RP_IDRequired — .env.localRequired — Convex deployment
AUTUMN_SECRET_KEYRequired — .env.localRequired — both
NEXT_PUBLIC_APP_URLRequired — .env.localRequired — both
NEXT_PUBLIC_APP_NAMEOptional — .env.localRequired — both
NEXT_PUBLIC_SUPPORT_EMAILOptionalOptional
NEXT_PUBLIC_PURCHASE_URLNot usedOptional — .env.local
NEXT_PUBLIC_SENTRY_DSNOptional, runtimeOptional, runtime
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKENOptional, build-timeOptional, build-time

Only NEXT_PUBLIC_* values reach the browser, and they are inlined into the bundle at build time — never put a secret behind that prefix. Convex deployment-side variables are read by the Convex backend at runtime and are never visible to the Next.js build.

Database and Backend

DATABASE_URL

Postgres edition only. Required, in .env.local.

The Postgres connection string. The app connects with pg (node-postgres), so any provider works — most hosted ones need sslmode=require.

DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"

NEXT_PUBLIC_CONVEX_URL

Convex edition only. Required, in .env.local.

Your Convex cloud deployment URL (….convex.cloud), printed by bun convex dev.

NEXT_PUBLIC_CONVEX_URL="https://your-deployment.convex.cloud"

NEXT_PUBLIC_CONVEX_SITE_URL

Convex edition only. Required, in .env.local.

The Convex HTTP Actions URL — the same deployment on the .convex.site domain. Take NEXT_PUBLIC_CONVEX_URL and swap .convex.cloud for .convex.site. Required by the auth proxy in lib/auth-server.ts.

NEXT_PUBLIC_CONVEX_SITE_URL="https://your-deployment.convex.site"

CONVEX_DEPLOYMENT

Convex edition only. Written for you, not set by hand.

bun convex dev writes it into .env.local to record which deployment the CLI talks to. It is not in .env.example.

Auth and Passkeys

BETTER_AUTH_SECRET

Required in both editions. Random secret used by Better Auth for session and token encryption.

Generate with:

openssl rand -base64 32

Set it in .env.local:

BETTER_AUTH_SECRET="…"

Set it on the Convex deployment. The JWKS private key is encrypted with it, so set this before generating JWKS:

bunx convex env set BETTER_AUTH_SECRET "$(openssl rand -base64 32)"

JWKS

Convex edition only. Required, on the Convex deployment.

The RS256 signing key set for Convex JWTs. convex/auth.config.ts reads the public key set from this variable; Better Auth signs tokens with the stored private key.

The stack ships a Convex action that generates the key pair if none exists and returns it in the exact shape the auth config expects. With bun convex dev running (so the functions are deployed):

# Prints the JWKS document set
bunx convex run betterAuth/auth:getLatestJwks

Set the output as the JWKS env var. On macOS/Linux you can do it in one step:

bunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks)"

On Windows, copy the printed JSON and paste it into the same command in place of the substitution.

To rotate keys, run bunx convex run betterAuth/auth:rotateKeys and set the returned value as JWKS again. Key rotation logs out all users — do it only when necessary, during low-traffic periods.

The Postgres edition has no equivalent: Better Auth runs inside Next.js and issues its own sessions, so there is nothing to generate or rotate.

BETTER_AUTH_URL

Convex edition only. Not required.

Present on the Convex deployment but unused at runtime — kept only for the Better Auth CLI. The runtime uses NEXT_PUBLIC_APP_URL instead, so you don't normally need to touch this.

BETTER_AUTH_URL="http://localhost:3000"

PASSKEY_RP_ID

Required in both editions.

The Relying Party ID for WebAuthn/passkeys — the bare public domain, no scheme or port. Use localhost for local dev; in production it must exactly match your deployed domain.

PASSKEY_RP_ID="localhost"
bunx convex env set PASSKEY_RP_ID "localhost"

For production with subdomains, use the root domain (e.g. example.com, not app.example.com) so passkeys work across all subdomains. NEXT_PUBLIC_APP_URL must be the matching https:// origin.

Email

EMAIL_FROM

Required in both editions. A sender address verified in Resend. Every email flow throws if this is missing.

Format: "Display Name <email@domain.com>".

EMAIL_FROM="Unstack Pro <noreply@yourdomain.com>"

Email is sent from Convex, so the variable belongs on the deployment:

bunx convex env set EMAIL_FROM "Unstack Pro <noreply@yourdomain.com>"

RESEND_API_KEY

Required in both editions. Your Resend API key for transactional email (verification, OTP, invitations).

RESEND_API_KEY="re_..."
bunx convex env set RESEND_API_KEY "re_..."

Billing

AUTUMN_SECRET_KEY

Required for billing in both editions. Your Autumn billing secret key (starts with am_sk_).

One place only — .env.local. The billing routes (app/api/autumn/[...all]/route.ts) read it at runtime.

AUTUMN_SECRET_KEY="am_sk_..."

AUTUMN_SECRET_KEY must be set in both environments. The Next.js billing routes (app/api/autumn/[...all]/route.ts) read it at runtime in the Node runtime, and the Convex auth hooks read it on the deployment for seat syncing and the Pro-plan gate. Setting it in only one place will break checkout/portal or seat syncing.

.env.local
AUTUMN_SECRET_KEY="am_sk_..."
bunx convex env set AUTUMN_SECRET_KEY "am_sk_..."

App Identity

NEXT_PUBLIC_APP_URL

Required in both editions.

The public URL of the Next.js app. Used for SEO metadata, the sitemap, email links, trusted origins, and the passkey origin. Use http://localhost:3000 for local dev.

NEXT_PUBLIC_APP_URL="http://localhost:3000"

.env.local only.

Also set it on the Convex deployment — Convex-side email links and the passkey origin read it there:

bunx convex env set NEXT_PUBLIC_APP_URL "http://localhost:3000"

NEXT_PUBLIC_APP_NAME

Display name shown throughout the UI and in transactional emails.

NEXT_PUBLIC_APP_NAME="Unstack Pro"

Optional in .env.local — a fallback name is used when it is unset.

Required in both places. Email templates run on Convex, so set it on the deployment too:

bunx convex env set NEXT_PUBLIC_APP_NAME "Unstack Pro"

NEXT_PUBLIC_SUPPORT_EMAIL

Optional in both editions, in .env.local.

Support address surfaced in the landing page / FAQ copy. Falls back to a placeholder if unset.

NEXT_PUBLIC_SUPPORT_EMAIL="support@yourcompany.com"

NEXT_PUBLIC_PURCHASE_URL

Convex edition only. Optional, in .env.local.

The checkout link behind the landing page's buy buttons (app/page.tsx). Falls back to # — a dead link — when unset. The Postgres edition does not read this variable at all; its purchase link is hardcoded.

NEXT_PUBLIC_PURCHASE_URL="https://buy.yourcompany.com/unstack-pro"

Error Monitoring

Sentry (optional)

Optional in both editions, in .env.local (or your host's environment).

Leave all of these unset to disable Sentry entirely — the SDK no-ops when NEXT_PUBLIC_SENTRY_DSN is missing, and builds skip source-map upload when SENTRY_AUTH_TOKEN is missing.

NEXT_PUBLIC_SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
SENTRY_AUTH_TOKEN=""
VariablePurpose
NEXT_PUBLIC_SENTRY_DSNSentry project DSN, read at runtime. When empty, Sentry is disabled.
SENTRY_ORGSentry org slug (build-time, for source maps).
SENTRY_PROJECTSentry project slug (build-time, for source maps).
SENTRY_AUTH_TOKENToken used to upload source maps at build time.

Complete Example

.env.local
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"

NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="Unstack Pro"
NEXT_PUBLIC_SUPPORT_EMAIL="support@yourcompany.com"

BETTER_AUTH_SECRET="replace-me--run: openssl rand -base64 32"

EMAIL_FROM="Unstack Pro <noreply@yourdomain.com>"
RESEND_API_KEY="re_..."

AUTUMN_SECRET_KEY="am_sk_..."

PASSKEY_RP_ID="localhost"

# Optional — Sentry (leave empty to disable)
NEXT_PUBLIC_SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
SENTRY_AUTH_TOKEN=""

That is the complete configuration — there is nothing else to set anywhere.

.env.local
NEXT_PUBLIC_CONVEX_URL="https://your-deployment.convex.cloud"
NEXT_PUBLIC_CONVEX_SITE_URL="https://your-deployment.convex.site"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="Unstack Pro"
NEXT_PUBLIC_SUPPORT_EMAIL="support@yourcompany.com"

# Optional — checkout link behind the landing page's buy buttons
NEXT_PUBLIC_PURCHASE_URL="https://buy.yourcompany.com/unstack-pro"

# Required in BOTH environments (also set on Convex)
AUTUMN_SECRET_KEY="am_sk_..."

# Optional — Sentry (leave empty to disable)
NEXT_PUBLIC_SENTRY_DSN=""
SENTRY_ORG=""
SENTRY_PROJECT=""
SENTRY_AUTH_TOKEN=""

And on the deployment:

# Auth
bunx convex env set BETTER_AUTH_SECRET "$(openssl rand -base64 32)"
bunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks)"

# Email
bunx convex env set RESEND_API_KEY "re_..."
bunx convex env set EMAIL_FROM "Unstack Pro <noreply@yourdomain.com>"

# Billing (also required in .env.local)
bunx convex env set AUTUMN_SECRET_KEY "am_sk_..."

# Application (same values as .env.local)
bunx convex env set NEXT_PUBLIC_APP_URL "http://localhost:3000"
bunx convex env set NEXT_PUBLIC_APP_NAME "Unstack Pro"

# Passkeys
bunx convex env set PASSKEY_RP_ID "localhost"

You can view and manage Convex environment variables in your Convex dashboard.

Checklist

Before deploying, make sure these are set:

.env.local (or your host's environment):

  • DATABASE_URL
  • NEXT_PUBLIC_APP_URL
  • BETTER_AUTH_SECRET
  • EMAIL_FROM (verified Resend sender)
  • RESEND_API_KEY
  • AUTUMN_SECRET_KEY
  • PASSKEY_RP_ID
  • NEXT_PUBLIC_APP_NAME (optional)
  • NEXT_PUBLIC_SUPPORT_EMAIL (optional)
  • Sentry vars (optional)

Next.js environment (.env.local / host):

  • NEXT_PUBLIC_CONVEX_URL
  • NEXT_PUBLIC_CONVEX_SITE_URL
  • NEXT_PUBLIC_APP_URL
  • NEXT_PUBLIC_APP_NAME
  • AUTUMN_SECRET_KEY
  • NEXT_PUBLIC_SUPPORT_EMAIL (optional)
  • NEXT_PUBLIC_PURCHASE_URL (optional)
  • Sentry vars (optional)

Convex deployment:

  • BETTER_AUTH_SECRET
  • JWKS
  • EMAIL_FROM (verified Resend sender)
  • RESEND_API_KEY
  • AUTUMN_SECRET_KEY
  • PASSKEY_RP_ID
  • NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_APP_NAME

Troubleshooting

"X is not defined" Errors

Confirm the variable is in .env.local and that the dev server was restarted afterwards. One trap: drizzle-kit does not load .env.local, so db:generate and db:migrate see DATABASE_URL as undefined when run bare — export the file into the shell first:

set -a && . ./.env.local && set +a && bunx drizzle-kit migrate

When Convex code throws that a variable is missing, consult createAuthOptions in convex/betterAuth/auth.ts for the full required set, and confirm the variable is set on the Convex deployment — not just in .env.local.

Billing Routes 500 or Seats Don't Sync

AUTUMN_SECRET_KEY is missing from .env.local, or the products were never pushed with bunx atmn push.

AUTUMN_SECRET_KEY is missing from one of the two environments. Set it in both .env.local (Next.js runtime) and on the Convex deployment.

Passkeys Not Working

PASSKEY_RP_ID must match your domain exactly (localhost for dev, the root domain in production), and NEXT_PUBLIC_APP_URL must be the matching origin. In the Convex edition, check that PASSKEY_RP_ID is on the deployment, since that is where Better Auth reads it.

Security Best Practices

  1. Never commit secrets: keep .env.local in .gitignore.
  2. Use different secrets for development and production.
  3. Restrict API keys to the minimum permissions necessary.
  4. Monitor access: review your database, backend, and Resend logs regularly.

Rotating BETTER_AUTH_SECRET invalidates existing sessions, so treat it as a logout-everyone operation and schedule it accordingly.

Rotate JWKS periodically with bunx convex run betterAuth/auth:rotateKeys, then set the output as JWKS — this logs everyone out, so schedule it for a low-traffic period.

Production Reminder: Always use HTTPS in production. Never use http:// URLs for NEXT_PUBLIC_APP_URL.

On this page