APP_URL, DATABASE_URL and AUTH_SECRET.
For a working setup, start with Self-hosting with Docker and come back here when you want to
turn on an optional feature.
How values are read
- The server reads variables from its process environment. In a source checkout it also loads a
.envfile from the workspace root. A variable already set in the environment always wins over the file. - An empty value counts as unset.
SMTP_HOST=""is the same as not settingSMTP_HOSTat all. - Values are validated at startup. If one is missing or malformed, the server stops and names the variable in its logs.
- Boolean variables accept
trueorfalse.1/0,yes/noandon/offalso work; any other value stops the server. - Changes take effect after a restart. With Docker Compose, run
docker compose up -dto recreate the container with the new environment.
PDFs are rendered by the app itself, so there is no printer service to configure. The v4 and early v5 variables
PRINTER_*, BROWSERLESS_* and CHROME_* are no longer read; you can remove them.Required
Server
Reverse-proxy trust
WithoutTRUSTED_PROXIES, the Node server ignores forwarded IP headers. Visitors behind one proxy then share its
IP-based limits, including sign-in and public resume password attempts. Set this variable to the actual proxy address
as seen by the app, not your public domain, visitors’ addresses or every private network.
0.0.0.0/0 or ::/0: a client could supply
its own identity and bypass rate limits. If another proxy or CDN is in front, configure that trust boundary too and
trust only the intermediary addresses you control. Vercel and Cloudflare use their deployment adapters to resolve
the client address; this variable configures the Node socket-based path. Cloudron supplies its proxy address when
no explicit value is set. See the safe Docker proxy examples.
Second domain and custom domains
Both are off unless set. Every account’s short link,/<username>, opens the resume its owner marked Primary resume in Share, with or without them.
For DNS, HTTPS, alias routing and the custom-domain provider endpoint, see
Sharing domains. Setting domain variables doesn’t create proxy routes or certificates.
Font mirror
For the legacy
WEB_FONT_BASE_URL, preserve the upstream font/<family>/<file>.woff paths below its base URL. Set the variable before the server starts;
restart or redeploy after changes. No web rebuild is needed for this server-side setting.
See Mirror Computer Modern fonts for the asset layout, deployment settings and verification.
Database
Sign-in
Social sign-in
Each provider appears on the sign-in page once both of its variables are set. See Single sign-on for callback URLs and provider setup.
For Apple, use your Services ID as
APPLE_CLIENT_ID and a signed client-secret JWT as APPLE_CLIENT_SECRET
(not the .p8 private key). Register APP_URL followed by /api/auth/callback/apple as the return URL.
Renew the JWT before it expires; Apple permits at most six months. Both variables must be set to enable the
provider and trust Apple’s https://appleid.apple.com POST callback origin. This browser flow does not need
an app bundle identifier in Better Auth.
Native app links
Association files are optional. Leave these unset to return 404 from their endpoints. Values are comma-separated; whitespace around each value is ignored.
The app must also configure its associated domains or Android intent filters for your HTTPS host. Links cover
/auth/native-callback, public resume URLs and /builder/*. /auth/native-callback provides a fallback when
the app does not open. Hosting these files alone does not verify device handoff.
Custom OAuth or OpenID Connect provider
Use these to sign in through your own identity provider, such as Authentik, Keycloak or Authelia. The provider is turned on when the client ID and secret are set together with either a discovery URL or all three manual endpoints. Its callback URL isAPP_URL followed by /api/auth/callback/custom.
SMTP_HOST, SMTP_USER, SMTP_PASS and SMTP_FROM are all set. Until then, emails aren’t sent. In production the
server logs only each email’s recipient and subject, never its link, because the link signs someone in. So without
SMTP, password reset, email verification and email changes don’t work, and passkeys (which need a verified email)
can’t be added. The app says so on those screens. In development (NODE_ENV not production) the full email,
link included, is printed to the server console. Each email goes out in the account’s interface language, or English while that
language has no translation for it.
Recover an account without email
If someone is locked out because they lost both their authenticator and their backup codes, you can turn off two-step verification for their account in the database, then ask them to sign in with their password and set it up again:Storage
Uploads such as profile pictures, files attached to applications and Assistant attachments are stored in one of three backends.Assistant attachments stay private with every backend. Local storage writes them in a separate namespace that public
upload routes never serve. General file uploads allow 10 MB; Assistant attachments allow 25 MB per file.
AI and Redis
For setup instructions and official provider guides, see Job search and AI. To let people bring their own keys, setENCRYPTION_SECRET. They can then add providers in
Settings → AI; see Connecting an AI provider.
Alternatively, configure a shared provider with AI_PROVIDER, AI_MODEL and AI_API_KEY (the key is optional for
Ollama and OpenAI-compatible servers). Server configuration takes precedence over personal providers for every AI
feature, including the Assistant. Settings show that AI is enabled globally and hide personal provider controls; the API
also rejects personal provider changes. Existing personal providers remain stored and become available again if you
remove the server configuration. Shared credentials stay in the environment and do not require ENCRYPTION_SECRET.
Redis is optional on a single server. Without it, everything works, with these limits:
- An Assistant reply that is interrupted by a page reload can’t be picked up again.
- Rate limits, Stop on a running Assistant reply and the check that counts each public resume view once an hour are kept in the memory of one server process.
- Live resume updates fall back to PostgreSQL
LISTEN/NOTIFY, sharing a single connection per server process across all active subscriptions.
REDIS_URL when you run more than one server process, or when you want replies to survive a reload. When Redis is
configured, the health endpoint also checks it.
For password-only authentication (redis-server --requirepass), leave the username empty —
redis://:<password>@<host> — or use the built-in ACL user, redis://default:<password>@<host>. A named username
only works when the server has a matching ACL user; otherwise Redis replies WRONGPASS invalid username-password pair. Passwordless (nopass) ACL users keep an explicitly empty password: redis://<user>:@<host>.
Saved AI providers and Assistant conversations are kept in PostgreSQL, so they remain available without Redis.
Career scheduling
Without
CRON_SECRET, the HTTP runner returns 404; an invalid bearer token returns 401. On Vercel, set the secret in the Production environment and redeploy so Vercel Cron can supply it. Users must still opt in to each schedule. See Career scheduling for deployment timing and verification, and Self-hosting on Vercel for its setup.
Web access
The built-in reader is active without credentials. Search and enhanced reading use one optional connection: Firecrawl, Tavily or Exa. People can select a provider and save one personal key in Settings → AI → Web access whenENCRYPTION_SECRET is configured. Personal connections use official cloud endpoints.
For a shared service, set WEB_ACCESS_PROVIDER and WEB_ACCESS_API_KEY. Only Firecrawl accepts a custom
WEB_ACCESS_API_URL, including a keyless self-hosted service. Server configuration takes precedence, settings say
Provided by the server, and personal changes are rejected. Stored personal connections become available again
when the shared configuration is removed. Shared connections do not require ENCRYPTION_SECRET.
Explicit generic configuration wins over legacy Firecrawl variables. Incomplete generic configuration, a missing key
for Tavily/Exa, or a custom URL for those providers stops startup instead of silently choosing another connection.
WEB_SEARCH_SEARXNG_URL is independent of the WEB_ACCESS_* variables, so shared and personal connections keep
working beside it. When it is set, Applications search, role discovery and assistant web search go to that SearXNG
instance for every user, with or without a web connection, and the instance must list json in search.formats.
Each of a user’s search countries is one SearXNG request, using its likely language and country, such as en-AU,
ja-JP or pt-BR. This applies to posting search, assistant search, background discovery and the connection test,
independently of the interface language. Reading still uses the web connection, then the built-in
reader, so sites that block plain fetches still need Firecrawl, Tavily or Exa. With it set, Test connection tests
search against SearXNG and reading against the connection. See
Search directly from Reactive Resume.
Connections enable Applications keyword search and assistant web tools. URL import uses the selected reader, then
falls back to the built-in reader on recoverable failures. Saving pasted text and manual preparation work without
either web-access or AI credentials. Search returns up to five links; choosing a result does not create an application
until the user reviews and saves it.
Test connection checks search and enhanced reading independently, without reader fallback. Rendering settings
does not call external services. Quota and authentication failures leave manual workflows available.
See Job search and AI for connection setup, Firecrawl deployment, optional SearXNG
search and backup guidance. Public page URLs remain protected against private destinations, redirects
and DNS rebinding. A remote reader must enforce these protections internally too. Search queries and selected URLs
go to the chosen service; resume data and AI keys do not.
Feature flags
All flags default tofalse.
Deployment aliases
On Vercel (whenVERCEL=1), the server fills in some variables from the ones Vercel’s integrations inject. A value
you set yourself always wins.
Vercel builds also read
ALLOW_PREVIEW_MIGRATIONS: preview builds refuse to run migrations unless it is true, so a
preview can’t change your production database by accident. See Self-hosting on Vercel.
Development and tooling only
These appear in.env.example but are not used by a running server:
See Development setup for working on the code.
Related pages
- Self-hosting with Docker: a complete setup with PostgreSQL.
- Deployment examples: reverse proxies, S3 storage, Redis and local AI.
- Single sign-on: provider setup for the sign-in variables.
Cloudflare Workers
SetCLOUDFLARE=1 in Wrangler. The runtime uses native R2 (STORAGE_BACKEND=r2), disables native image processing, and receives its PostgreSQL connection through the HYPERDRIVE binding. Keep REDIS_URL unset; SQLite Durable Objects provide shared limits, live updates, and cancellation. See Self-hosting on Cloudflare for required bindings, secrets, migrations, and runtime differences.