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:
- The Next.js environment —
.env.localin local development, or your host's environment variables (e.g. Vercel) in production. - 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.localThat'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
| Variable | Postgres edition | Convex edition |
|---|---|---|
DATABASE_URL | Required — .env.local | Not used |
NEXT_PUBLIC_CONVEX_URL | Not used | Required — .env.local |
NEXT_PUBLIC_CONVEX_SITE_URL | Not used | Required — .env.local |
CONVEX_DEPLOYMENT | Not used | Written into .env.local by convex dev |
BETTER_AUTH_SECRET | Required — .env.local | Required — Convex deployment |
JWKS | Not used | Required — Convex deployment |
BETTER_AUTH_URL | Not used | Convex deployment; unused at runtime |
EMAIL_FROM | Required — .env.local | Required — Convex deployment |
RESEND_API_KEY | Required — .env.local | Required — Convex deployment |
PASSKEY_RP_ID | Required — .env.local | Required — Convex deployment |
AUTUMN_SECRET_KEY | Required — .env.local | Required — both |
NEXT_PUBLIC_APP_URL | Required — .env.local | Required — both |
NEXT_PUBLIC_APP_NAME | Optional — .env.local | Required — both |
NEXT_PUBLIC_SUPPORT_EMAIL | Optional | Optional |
NEXT_PUBLIC_PURCHASE_URL | Not used | Optional — .env.local |
NEXT_PUBLIC_SENTRY_DSN | Optional, runtime | Optional, runtime |
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKEN | Optional, build-time | Optional, 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 32Set 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:getLatestJwksSet 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_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.
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=""| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SENTRY_DSN | Sentry project DSN, read at runtime. When empty, Sentry is disabled. |
SENTRY_ORG | Sentry org slug (build-time, for source maps). |
SENTRY_PROJECT | Sentry project slug (build-time, for source maps). |
SENTRY_AUTH_TOKEN | Token used to upload source maps at build time. |
Complete Example
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.
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_URLandNEXT_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 migrateWhen 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
- Never commit secrets: keep
.env.localin.gitignore. - Use different secrets for development and production.
- Restrict API keys to the minimum permissions necessary.
- 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.