Reactive Resume is configured entirely through environment variables. This page lists every variable the server reads,
grouped by what it controls. Only three are required: 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#
| Variable | Description |
|---|---|
APP_URL | The public address people use to reach your instance, for example https://resume.example.com. Must start with http:// or https://. Used for sign-in redirects, OAuth callbacks, links in emails, public resume links, social previews and upload URLs. With an https:// address, session cookies are marked secure. |
DATABASE_URL | PostgreSQL connection string: postgresql://USER:PASSWORD@HOST:5432/DATABASE. Must start with postgres:// or postgresql://. URL-encode special characters in the password. Add provider options such as ?sslmode=require when your database needs them. |
AUTH_SECRET | Secret used to sign sessions and encrypt sign-in data. Generate one with openssl rand -hex 32. In production it must be at least 32 characters, and the example value from .env.example stops the server at startup. |
Keep AUTH_SECRET the same for the life of your instance. Changing it signs everyone out and makes data encrypted
with it unreadable, including two-factor authentication secrets and the keys that sign API and MCP access tokens.
Server#
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Port the production server listens on. The official image sets it to 3000. If you change it, update your port mapping and health check to match. |
NODE_ENV | production in the image | When production, the server listens on PORT and turns on rate limiting. Leave it as the image sets it. |
SERVER_PORT | 3001 | Port the server listens on when NODE_ENV is not production, which is the local development setup. Ignored by the image. |
ROOT_RESUME_ID | unset | Shows one public resume at / instead of the home page. See Show one resume at your root address. |
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.
| Variable | Default | Description |
|---|---|---|
APP_ALIAS_URL | unset | A second domain, such as https://rx.cv, that serves the same public pages as APP_URL: resumes, short links, the home page. Sign-in and the editor redirect to APP_URL, and search engines are pointed there. When set, Share copies links on this domain. Point the domain at this server. |
CUSTOM_DOMAIN_TARGET | unset | The host people point their own domain’s CNAME record at, such as domains.example.com. It must resolve to this server. |
CUSTOM_DOMAIN_PROVIDER_SECRET | unset | At least 32 characters. Traefik’s HTTP provider sends it in an X-Provider-Secret header to read /api/internal/traefik?service=<service>&entryPoint=<entry point>&certResolver=<resolver>, which returns a router, and so a certificate, for every verified domain. Custom domains need both this and CUSTOM_DOMAIN_TARGET. |
OLACV_API_KEY | unset | An OlaCV API token. With FLAG_OLACV_SUGGESTION, Share checks which .cv names are free before suggesting one. The server calls https://ola.cv/api/v1/domains/check with the account’s candidate names only; the token never reaches the browser. |
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#
| Variable | Default | Description |
|---|---|---|
WEB_FONT_BASE_URL | unset | Absolute HTTP(S) base URL for the five Computer Modern families during server-side PDF rendering, for example https://fonts.example.com/cm. No query string or fragment. Unset or empty keeps the original URLs. |
Preserve the upstream font/<family>/<file>.woff paths below this base URL. Set the variable before the server starts;
restart or redeploy after changes. No web rebuild is needed for this server-side setting.
This server-only variable is not exposed to the Vite browser app. Browser PDF rendering and font-picker previews still use their original URLs. Google-hosted families and fallback fonts are not rewritten, including during server rendering. This setting does not provide offline or Google-free font loading.
See Mirror Computer Modern fonts for the asset layout, deployment settings and verification.
Database#
| Variable | Default | Description |
|---|---|---|
DATABASE_MIGRATION_URL | DATABASE_URL | Separate connection string for the migrations that run at startup. Set it when DATABASE_URL goes through a connection pooler (such as PgBouncer or a provider’s pooled endpoint) that cannot run migrations. |
DATABASE_POOL_MAX | 10 | Maximum number of database connections each server process opens, from 1 to 100. |
STRICT_SCHEMA_CHECK | false | After migrations, the server compares the live schema with what the migrations expect. By default a mismatch is logged and the server keeps starting. Set true to refuse to start instead. |
Sign-in#
| Variable | Default | Description |
|---|---|---|
BETTER_AUTH_API_KEY | unset | Connects the instance to the Better Auth dashboard service. Most instances leave it empty. |
OPENAI_APPS_CHALLENGE_TOKEN | unset | Domain verification token from the OpenAI plugin portal, served as plain text at /.well-known/openai-apps-challenge. Only needed to publish your instance’s MCP server as a ChatGPT or Codex plugin. |
BETTER_AUTH_INTERNAL_URL | http://127.0.0.1:$PORT | Address the server uses to reach itself when it verifies OAuth access tokens from API and MCP clients. Set it only if the server cannot reach itself on 127.0.0.1 at PORT. |
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.
| Variables | Provider |
|---|---|
APPLE_CLIENT_ID, APPLE_CLIENT_SECRET | Apple |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | |
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | GitHub |
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET |
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.
| Variable | Description |
|---|---|
MOBILE_IOS_APP_IDS | Apple application identifiers, such as ABCDE12345.me.rxresu (TEAMID.bundle.id). Serves /.well-known/apple-app-site-association with applinks and webcredentials. |
MOBILE_ANDROID_PACKAGES | Android package names, such as me.rxresu. Together with the fingerprints below, serves /.well-known/assetlinks.json. |
MOBILE_ANDROID_CERT_SHA256 | Colon-separated SHA-256 certificate fingerprints (32 hexadecimal byte pairs). Every listed package trusts every listed fingerprint; include the Play app-signing certificate for Play builds. |
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 is APP_URL followed by /api/auth/callback/custom.
| Variable | Default | Description |
|---|---|---|
OAUTH_PROVIDER_NAME | Custom OAuth | Name shown on the sign-in button. |
OAUTH_CLIENT_ID | unset | Client ID issued by your provider. |
OAUTH_CLIENT_SECRET | unset | Client secret issued by your provider. |
OAUTH_DISCOVERY_URL | unset | The provider’s .well-known/openid-configuration address. Preferred for OpenID Connect providers. |
OAUTH_AUTHORIZATION_URL | unset | Authorization endpoint. Use with the next two instead of a discovery URL. |
OAUTH_TOKEN_URL | unset | Token endpoint. |
OAUTH_USER_INFO_URL | unset | User info endpoint. |
OAUTH_SCOPES | openid profile email | Space-separated scopes to request. |
Email#
Reactive Resume sends email for account verification, password resets and email changes. Sending turns on only when
SMTP_HOST, SMTP_USER, SMTP_PASS and SMTP_FROM are all set. Until then, each email is written to the server log
instead, so you can still copy verification links from there.
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | unset | SMTP server hostname. |
SMTP_PORT | 587 | SMTP server port. |
SMTP_USER | unset | SMTP username. |
SMTP_PASS | unset | SMTP password. |
SMTP_FROM | unset | Sender address, for example Reactive Resume <noreply@example.com>. |
SMTP_SECURE | false | true connects with TLS from the start (usually port 465). false requires a successful STARTTLS upgrade and fails if the server does not support it (usually port 587). |
Storage#
Uploads such as profile pictures, files attached to applications and Assistant attachments are stored in one of three backends.
| Variable | Default | Description |
|---|---|---|
STORAGE_BACKEND | automatic | local, s3, blob or r2 (Workers only). When unset: r2 on Cloudflare Workers; otherwise s3 if S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY and S3_BUCKET are all set, blob on Vercel and local elsewhere. |
STORAGE_HEALTHCHECK_TIMEOUT_MS | 5000 | Total storage check deadline for /api/health, in milliseconds. Includes both S3 PUT and DELETE requests. Increase for slower storage; set your external probe timeout higher than this value. |
LOCAL_STORAGE_PATH | /app/data in the image | Folder for local uploads. Must be an absolute path. In a source checkout it defaults to data/ in the repository. The server checks that it is writable at startup and refuses to start if it is not. |
S3_ACCESS_KEY_ID | unset | Access key for S3 or an S3-compatible service. |
S3_SECRET_ACCESS_KEY | unset | Secret key. |
S3_BUCKET | unset | Bucket name. The bucket can stay private: the app reads objects with its own credentials and serves them itself. |
S3_REGION | us-east-1 | Bucket region. |
S3_ENDPOINT | AWS | Endpoint for non-AWS services, for example https://<account>.r2.cloudflarestorage.com or http://seaweedfs:8333. |
S3_FORCE_PATH_STYLE | false | true for path-style addresses (https://endpoint/bucket), which MinIO and SeaweedFS need. false for virtual-hosted addresses (https://bucket.endpoint), used by AWS S3 and Cloudflare R2. |
BLOB_READ_WRITE_TOKEN | unset | Vercel Blob token. On Vercel it is injected when you connect a Blob store. |
BLOB_STORE_ID | unset | Vercel Blob store ID, when the token alone does not identify the store. |
DEPLOYMENT_NAMESPACE | default | Prefix for Redis keys and Blob paths, so several instances can share one Redis or Blob store without mixing data. Letters, numbers, ., _ and - only. On Vercel it is production, or the branch address on previews. |
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.
Switching backends does not move files that are already stored. Copy them yourself before you switch.
AI and Redis#
For setup instructions and official provider guides, see Job search and AI.
To let people bring their own keys, set ENCRYPTION_SECRET. They can then add providers in
Settings → AI & developer; 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). 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.
| Variable | Default | Description |
|---|---|---|
ENCRYPTION_SECRET | unset | Encrypts personal AI and web-access keys. At least 32 characters; generate one with openssl rand -hex 32 and keep it different from AUTH_SECRET. A shorter value stops the server at startup. Required for saving personal keys. |
AI_PROVIDER | unset | Shared provider identifier, such as openai, anthropic, gemini, ollama or openai-compatible. Uses the same providers as the personal provider settings. |
AI_MODEL | unset | Shared model name, such as gpt-5-mini. Required with AI_PROVIDER. |
AI_API_KEY | unset | Shared provider key. Required with AI_PROVIDER, except for Ollama. Never returned to the browser. |
AI_BASE_URL | provider default | Optional shared provider endpoint; required for openai-compatible. Local or private endpoints need FLAG_ALLOW_UNSAFE_AI_BASE_URL=true. |
REDIS_URL | unset | Redis connection string, redis:// or rediss://. Optional. |
AI_TEST_TIMEOUT_MS | 30000 | How long Save and test waits for a provider to answer, in milliseconds. Raise it for local models that load slowly on first use. |
AI_REQUEST_TIMEOUT_MS | 300000 | How long a single AI action (importing a resume, reviewing, improving, reading a posting) waits for a provider, in milliseconds. Raise it for slow local models. |
Changing ENCRYPTION_SECRET makes every saved AI and web-access key unreadable. People then need to enter their keys
again.
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.
Set 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.
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 & developer →
Web access when ENCRYPTION_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.
| Variable | Default | Description |
|---|---|---|
WEB_ACCESS_PROVIDER | unset | One selected provider: firecrawl, tavily or exa. |
WEB_ACCESS_API_KEY | unset | Shared provider key. Required except for Firecrawl with an explicit custom URL. |
WEB_ACCESS_API_URL | Firecrawl Cloud | Optional Firecrawl base URL without /v2, e.g. http://localhost:3102. Private service URLs are operator-controlled. Tavily and Exa always use their official endpoints. |
WEB_SEARCH_SEARXNG_URL | unset | Optional SearXNG base URL without /search, e.g. http://searxng:8080. Sends every web search there instead of the web connection; reading is unchanged. Private service URLs are operator-controlled. |
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 to false.
| Variable | When set to true |
|---|---|
FLAG_DISABLE_SIGNUPS | No new accounts can be created, by email, by social sign-in or as a guest. Existing accounts keep working. Create your own account first. |
FLAG_DISABLE_EMAIL_AUTH | Turns off email and password sign-in and sign-up, along with forgot password and reset password. People sign in with social or custom OAuth providers only, so configure at least one first. |
FLAG_ENABLE_GUEST_LOGIN | Shows Continue as guest on the sign-in page, so people can build and download a resume without an account. A guest who later signs up or signs in keeps their resumes, cover letters and applications. Guest work is lost if they sign out first. Has no effect while FLAG_DISABLE_SIGNUPS is true. |
FLAG_DISABLE_IMAGE_PROCESSING | Stores uploaded pictures as they are. Normally pictures are resized to fit 800 × 800 pixels and saved as JPEG. Useful on low-powered hardware such as a Raspberry Pi. |
FLAG_DISABLE_API_RATE_LIMIT | Turns off API and authentication rate limiting, including sign-in, sign-up, OAuth, requests made with API keys, PDF export and AI requests. Intended for test installations. |
FLAG_ALLOW_UNSAFE_AI_BASE_URL | Lets AI providers use http:// addresses and private or local network addresses, such as an Ollama server on your network. Without it, a provider’s base URL must be public https://. |
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI | Lets MCP and OAuth clients that register themselves use any redirect address, including custom schemes and private hosts. By default a redirect must go to your instance’s own address, an http:// loopback address (localhost, 127.0.0.1, ::1) or a public https:// address. |
FLAG_OLACV_SUGGESTION | Suggests registering a .cv domain made from the account’s username or name at OlaCV to people who haven’t connected a custom domain. Needs custom domains to be on. With OLACV_API_KEY, only a name OlaCV reports as free and not premium is suggested, with its price. |
Only turn on the two FLAG_ALLOW_UNSAFE_* flags on an instance where you trust every user. On a shared instance, an
unsafe AI base URL lets users make your server call internal network addresses, and an unsafe redirect URI can be used
for phishing or to steal access tokens.
Deployment aliases#
On Vercel (when VERCEL=1), the server fills in some variables from the ones Vercel’s integrations inject. A value
you set yourself always wins.
| Variable | Filled from |
|---|---|
APP_URL | https:// plus VERCEL_PROJECT_PRODUCTION_URL in production, or VERCEL_URL on previews |
DATABASE_URL | POSTGRES_URL |
DATABASE_MIGRATION_URL | DATABASE_URL_UNPOOLED, then POSTGRES_URL_NON_POOLING |
REDIS_URL | KV_URL |
STORAGE_BACKEND | blob, unless S3 credentials are set |
DEPLOYMENT_NAMESPACE | production, or VERCEL_BRANCH_URL on previews |
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:
| Variable | Used by |
|---|---|
GOOGLE_CLOUD_API_KEY | The script that regenerates the font list. |
CROWDIN_PROJECT_ID, CROWDIN_API_TOKEN | Translation sync tooling. |
COVER_LETTER_TEST_DATABASE_URL, OAUTH_TEST_DATABASE_URL, INTEGRATIONS_TEST_DATABASE_URL | Test suites that need a disposable database. Integration credential tests create and remove an isolated schema. |
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#
Set CLOUDFLARE=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.