ShipKit docs
Reference

Environment variables

What each one does, which are required, and which fail quietly when wrong.

Everything lives in one .env at the repo root. Bun and Next only auto-load a .env from the process's own working directory, and Turbo runs each app with its cwd set to that app, so every entrypoint imports @workspace/env/load-root-env first. That helper walks up to eight levels looking for the nearest .env and never overwrites a variable that is already set, so real environment variables and CI-passed values always win.

Validation is Zod, in packages/env/src/server.ts and client.ts. Anything required is checked at import time and fails the process with a readable error rather than a runtime undefined.

Required

Three, and nothing starts without them.

VariableNotes
BETTER_AUTH_SECRETopenssl rand -base64 32. Changing it invalidates every existing session.
NEXT_PUBLIC_APP_URLThe origin, with no /api suffix.
NEXT_PUBLIC_API_URLThe origin plus /api.

The two URLs are the single most expensive thing to get wrong. Better Auth appends /api/auth itself, and the Eden treaty client already carries /api/v1. Point NEXT_PUBLIC_APP_URL at a URL that ends in /api and every request goes to /api/api/auth/... and 404s, with nothing in the logs saying why.

In production, web, admin and api sit behind one proxy: APP_URL is the single origin, API_URL is that origin plus /api. In local development the apps run on separate ports, and APP_URL points at the API origin so auth and treaty calls reach the handler.

Fails quietly when wrong

These validate fine and change behaviour. Each one has cost somebody an afternoon.

VariableSymptom when wrong
TRUSTED_ORIGINSFeeds Better Auth's trusted origins. Disagreeing with the real host turns a correct password into "invalid credentials".
CORS_ORIGINSFeeds @elysiajs/cors. Unset falls back to the localhost dev ports, which looks fine locally and blocks everything in production.
USE_SECURE_COOKIESMust be true behind HTTPS. Read by both packages/auth and the admin's cookie-name lookup, so a mismatch means the admin cannot find the session it was just issued.
REDIS_DBShared Redis with a shared index means one app's FLUSHDB empties another's queues.
S3_ENDPOINTWins over the R2_* values. Leftover R2 credentials plus a set endpoint writes test objects locally, which is the intended safety, but the reverse surprises people.

Data stores

VariableDefaultNotes
POSTGRES_HOSTlocalhost
POSTGRES_PORT5432
POSTGRES_USERpostgres
POSTGRES_PASSWORDpostgres
POSTGRES_DBshipkit
REDIS_HOSTlocalhost
REDIS_PORT6379
REDIS_PASSWORDnone
REDIS_DB0Give each app its own index
REDIS_TLSfalse
REDIS_MAX_RETRIES3
REDIS_CONNECT_TIMEOUT10000ms
REDIS_KEEPALIVE30000ms

Pool tuning is optional and read only where it is used: DB_POOL_MAX, DB_POOL_MIN, DB_IDLE_TIMEOUT, DB_CONNECT_TIMEOUT, DB_MAX_LIFETIME, DB_STATEMENT_TIMEOUT, DB_IDLE_IN_TRANSACTION_TIMEOUT, DB_QUERY_TIMEOUT.

Optional integrations

Each block is inert until its variables are set. Nothing here blocks a first run.

FeatureVariablesBehaviour when unset
Google sign-inGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRETButton hidden. Both are needed; one alone does nothing.
GitHub sign-inGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRETSame
PaymentsPOLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_ORGANIZATION_ID, POLAR_PRODUCT_ID_*Checkout unavailable
Grace periodENTITLEMENTS_GRACE_DAYSDefaults to 7 days after a payment lapses
EmailRESEND_API_KEY, EMAIL_FROMNo mail sent; magic-link sign-in stays off
FilesS3_* or R2_*getStorage() throws a named error listing the variables
AIANTHROPIC_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, AI_CHAT_MODEL, AI_EMBEDDING_MODELAI routes unavailable
ErrorsSENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN, SENTRY_RELEASE, SENTRY_TRACES_SAMPLE_RATENothing reported
AnalyticsNEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST, POSTHOG_API_KEY, POSTHOG_HOSTNothing tracked. The server key falls back to the public one.
LoggingLOG_LEVELinfo in production, debug otherwise
SeedingSEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORD, SEED_ADMIN_NAMEDemo defaults, and the seed refuses to run them in production

Declared but not wired

OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME validate and can be passed through, but no exporter ships and nothing emits spans. They are a reservation, not a feature. See observability.

Build-time against run-time

NEXT_PUBLIC_* variables are inlined into the client bundle at build time. Changing one in the environment of a running process does nothing; the app has to be rebuilt. The same applies to NODE_ENV, which Bun inlines, and which is why the deployment guide insists it be exported before the first build command rather than only in the service file.

On this page