Environment
One root .env feeds every app.
There is one .env at the repo root. Every app loads it, and packages/env validates it with zod, so a bad value fails at start with a clear message. .env.example lists every key with its default, .env itself is git-ignored.
Urls
| Key | What it is |
|---|---|
API_URL | how the Next servers reach the api, an internal address on docker |
API_PUBLIC_URL | the api url the browser can reach |
WEB_URL | the landing page |
DASHBOARD_URL | the dashboard |
DOCS_URL | this site. Read by apps/docs and also by apps/web, which links to it. Not part of the packages/env schema. Set it in production: without it the landing page links to the docs files in the repo on GitHub |
Database and auth
| Key | What it is |
|---|---|
DATABASE_URL | postgres connection string |
POSTGRES_PORT | host port of the local docker postgres |
SEED | demo fills an empty database with sample data, none never seeds |
BETTER_AUTH_SECRET | at least 32 characters, no default: openssl rand -base64 32. The api does not start without it |
ADMIN_EMAIL, ADMIN_PASSWORD | the first admin, created or reset on every api start |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | optional github sign-in |
Models and extension
| Key | What it is |
|---|---|
AI_MODEL | provider:model, default groq:openai/gpt-oss-120b. Providers: groq, openai, anthropic |
GROQ_API_KEY | needed for the default model. Without any matching key the ai endpoints answer "No AI provider key is configured" and the rest keeps working |
OPENAI_API_KEY | optional. Embeddings (text-embedding-3-small) for similar-question suggestions, see how skiffr learns |
ANTHROPIC_API_KEY | only if AI_MODEL points at an anthropic: model |
AI_DAILY_CALLS | model requests one user may make per UTC day, default 600. Chat and embedding requests both count. 0 means no limit. Counted in the database, so it holds across restarts and replicas. Digits only, at most 2147483647 |
AI_DAILY_TOKENS | input plus output tokens one user may spend per UTC day, default 1000000. 0 means no limit. Checked before each call and added after it, so a few calls in flight at the limit can overshoot it slightly |
AI_GLOBAL_DAILY_CALLS | model requests the whole server may make per UTC day, all users together, default 0 (no limit). Set it on a public instance: it caps the spend of many free accounts together |
CLIENT_IP_HEADER, TRUSTED_PROXY_COUNT | where the api reads the client address for the sign-in and sign-up limits. Defaults x-forwarded-for and 1: the api sits behind one proxy that appends the address it saw (Traefik on Dokploy does). Use 0 when the api is reached directly, so the header is ignored and the connection's address is used, or the number of proxies when there are more |
EXTENSION_ORIGINS | optional, comma separated chrome-extension://<id> origins allowed to call the api. Unset allows any extension holding a valid token |
Error reports, logs, metrics, model call traces and page analytics (GLITCHTIP_DSN, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, UMAMI_URL, UMAMI_WEBSITE_ID, AI_TRACE_CAPTURE_CONTENT) are optional and described in Observability.
Gmail job emails (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GMAIL_ENCRYPTION_KEY and friends) are optional and described in Gmail job emails.
Tailoring your CV through Reactive Resume (CV_ENCRYPTION_KEY) is optional and described in CV tailoring.
Watching careers pages and feeds (WATCH_SCHEDULER, WATCH_REFRESH_MINUTES) needs no keys and is described in Job sources.
Every signed-in call is also rate limited per user in the api's memory (reads, writes, writes that call the embedding provider, model calls, uploads and anything that fetches the network each have their own bucket). With several replicas each one counts on its own, so the daily budget above is the real cost control. Sign-in and sign-up are limited per client address by better-auth in production, counted in the database (old rows are pruned hourly). They are not limited per account, so one email tried from many addresses is not slowed. Requests to made-up /auth/* paths each write a row until it is pruned: cap them at the proxy if that matters to you.
What each provider receives is listed in privacy and data flow.
Keys for modules you left out of the project are not in .env.example. Adding a key means adding it to the zod schema in packages/env first.