skiffr

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

KeyWhat it is
API_URLhow the Next servers reach the api, an internal address on docker
API_PUBLIC_URLthe api url the browser can reach
WEB_URLthe landing page
DASHBOARD_URLthe dashboard
DOCS_URLthis 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

KeyWhat it is
DATABASE_URLpostgres connection string
POSTGRES_PORThost port of the local docker postgres
SEEDdemo fills an empty database with sample data, none never seeds
BETTER_AUTH_SECRETat least 32 characters, no default: openssl rand -base64 32. The api does not start without it
ADMIN_EMAIL, ADMIN_PASSWORDthe first admin, created or reset on every api start
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECREToptional github sign-in

Models and extension

KeyWhat it is
AI_MODELprovider:model, default groq:openai/gpt-oss-120b. Providers: groq, openai, anthropic
GROQ_API_KEYneeded 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_KEYoptional. Embeddings (text-embedding-3-small) for similar-question suggestions, see how skiffr learns
ANTHROPIC_API_KEYonly if AI_MODEL points at an anthropic: model
AI_DAILY_CALLSmodel 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_TOKENSinput 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_CALLSmodel 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_COUNTwhere 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_ORIGINSoptional, 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.

On this page