Environment variables
Everything in apps/api/.env.example, grouped by what it turns on. The web shells need only two.
The API's env file is the source of truth for almost every adapter choice. Start by copying the example:
cp apps/api/.env.example apps/api/.envThen fill in what you need. Only two variables are strictly required to boot the API:
| Var | Required | Default |
|---|---|---|
DATABASE_URL | yes | — |
BETTER_AUTH_SECRET | yes | change-me-in-prod |
BETTER_AUTH_SECRET signs every session token. In prod, generate a long random string — openssl rand -hex 32 is fine. If you leak or rotate it, every existing session logs out.
Database
| Var | What it does |
|---|---|
DATABASE_URL | Postgres connection string. Used at runtime by the API and by migrations (Prisma or drizzle-kit). |
DIRECT_DATABASE_URL | Optional non-pooled URL. Set this if DATABASE_URL points at a transaction pooler (PgBouncer, PlanetScale psdb) — migration tools need direct connections for advisory locks. |
Auth & origins
| Var | Default | What it does |
|---|---|---|
BETTER_AUTH_SECRET | change-me-in-prod | Session signing key. Required in every environment. |
MAGIC_LINK_TTL_MIN | 15 | How long a magic link stays valid after it's minted. |
API_ORIGIN | http://localhost:4002 | Where the API answers. Used in mail templates and OAuth redirects. |
WEB_ORIGIN | http://localhost:4001 | Primary authenticated-app origin (allowed by CORS, trusted by better-auth). |
WWW_ORIGIN | http://localhost:4000 | Marketing site origin. |
ADDITIONAL_WEB_ORIGINS | http://localhost:4003 | Comma-separated list of extra allowed web origins. Handy when running both web-tanstack and web-next against the same API. |
PORT | 4002 | Port the API listens on. Change this if you change API_ORIGIN. |
OAuth providers (optional)
better-auth only registers providers whose client id and secret are both set. Leave them unset to disable:
GOOGLE_CLIENT_ID+GOOGLE_CLIENT_SECRETAPPLE_CLIENT_ID+APPLE_CLIENT_SECRET
Redirect URIs to register with each provider: ${API_ORIGIN}/v1/auth/callback/google and ${API_ORIGIN}/v1/auth/callback/apple.
| Var | What it does |
|---|---|
RESEND_API_KEY | Unset = ConsoleMailer (logs links to stdout + exposes them at GET /v1/dev/last-magic-link). Set = Resend in prod. |
RESEND_FROM | Sender name + address. Format: Orbit <hello@yourdomain.com> |
RESEND_SEND_IN_DEV | Set to 1 to force Resend even in dev — useful for template QA. |
Billing
BILLING_PROVIDER is the master switch. When unset or set to noop, the billing routes return a disabled-state response and no provider SDK is constructed. Set it to one of stripe / polar / dodo to wire the matching adapter.
Plan catalog
BILLING_PLANS_JSON is a JSON array of plan descriptors rendered on the billing settings page. Keep it small — once you outgrow a handful of plans, replace the env-backed source in composition.ts with a DB-backed provider.
BILLING_PLANS_JSON='[ { "key": "pro", "name": "Pro", "priceId": "price_...", "unitAmount": 800, "currency": "usd", "interval": "month", "intervalCount": 1, "features": ["Unlimited teams", "Priority email"] }]'Per-provider secrets
| Provider | Vars |
|---|---|
| Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
| Polar | POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_SERVER (sandbox | production) |
| Dodo | DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_ENVIRONMENT (test_mode | live_mode) |
Background jobs
JOBS_PROVIDER picks the adapter for the JobQueue and JobRuntime ports. Leave unset or set to noop to boot without background work; services that would enqueue a job 409 instead.
| Var | Scope | What it does |
|---|---|---|
JOBS_PROVIDER | both | graphile (Postgres-backed, self-hosted) / qstash (Upstash managed HTTP) / noop. |
WORKER_DATABASE_URL | graphile | Optional direct/session URL for LISTEN/NOTIFY. Fallback is DATABASE_URL. |
JOBS_CONCURRENCY | graphile | Parallelism for this worker instance. Default 2. |
QSTASH_TOKEN | qstash | Publish-side API token from the Upstash console. |
QSTASH_CURRENT_SIGNING_KEY | qstash | Current webhook-verification key (rotated by QStash). |
QSTASH_NEXT_SIGNING_KEY | qstash | Next webhook-verification key; both are checked on delivery. |
QSTASH_CALLBACK_URL | qstash | Public base URL of this API. Must be internet-reachable in dev (smee, ngrok, tunnel). |
Rate limiting
RATE_LIMIT_PROVIDER picks the adapter for the RateLimiter port. Leave unset or set to memory for the in-process sliding-window bucket; set to upstash for a Redis-backed limiter shared across workers and serverless instances.
| Var | Scope | What it does |
|---|---|---|
RATE_LIMIT_PROVIDER | all | noop / memory (default) / upstash / unkey. |
UPSTASH_REDIS_REST_URL | upstash | REST URL for the Upstash Redis database. |
UPSTASH_REDIS_REST_TOKEN | upstash | REST token with read/write access. |
UNKEY_ROOT_KEY | unkey | Root API key from the Unkey dashboard. |
UNKEY_RATELIMIT_NAMESPACE | unkey | Namespace prefix. Defaults to orbit. |
The memory adapter is single-process only — state is not shared across workers, is lost on restart, and does NOT protect a horizontally scaled or serverless deploy. The API warns at boot if it's used in production. See /docs/integrations/rate-limiting for the full rundown.
Uploads (optional)
UPLOADTHING_TOKEN enables the signed-upload endpoints. Leave unset to disable uploads entirely — the UI hides the avatar upload affordance when it sees the disabled state.
Waitlist mode (optional)
IS_WAITLIST_ENABLED—truereplaces the final step of/onboardingwith a "you're on the list" screen instead of creating a workspace.POST /v1/waitlistaccepts submissions regardless.WAITLIST_ADMIN_SECRET— If set,POST /v1/waitlist/acceptwithAuthorization: Bearer <secret>marks an entry approved.
Webhook tunneling (dev)
| Var | What it does |
|---|---|
SMEE_URL | Your personal smee.io URL. Register it as your provider's webhook endpoint; the apps/webhook-tunnel workspace reads this and forwards POSTs to the local API. |
SMEE_TARGET_PATH | Route to forward to. Defaults to /v1/billing/webhooks/stripe — switch to /polar or /dodo for the other adapters. |
Seed overrides (optional)
The seed script reads a handful of env vars so you can point it at your own email and workspace slug in dev:
SEED_OWNER_EMAIL— defaultowner@wereorbit.comSEED_OWNER_NAME— defaultDemo OwnerSEED_WORKSPACE_SLUG— defaultdemoSEED_WORKSPACE_NAME— defaultDemo
The web shells
The TanStack and Next shells need only two build-time vars, baked into the bundle by Vite / Next at build time:
| Var | What it does |
|---|---|
VITE_API_URL | Base URL the client hits for REST and WebSocket traffic. |
VITE_WEB_URL | Canonical origin for the authenticated app — used in email links and share URLs. |
The marketing site (apps/www) needs only VITE_WEB_URL, for the sign-in and "get access" buttons.