Skip to content

Environment

The server reads its configuration from the environment (or .env next to it) when it starts. A missing or malformed required value stops it with a message naming the variable.

Variable Default
DATABASE_URL required Postgres connection string
DATABASE_STATEMENT_TIMEOUT 15 Seconds a query may run before Postgres cancels it, so a slow one gives its connection back; a transaction may also sit idle a minute at most. Migrations and the sweep’s long work aren’t bound by it. 0 sends no limit: behind a pooler that refuses startup parameters (PgBouncer), set statement_timeout on the database role instead
S3_ENDPOINT required Where the server reaches storage
S3_PUBLIC_ENDPOINT S3_ENDPOINT Where browsers reach storage, when that differs (Docker Compose)
S3_REGION us-east-1 Storage region
S3_BUCKET required Bucket; created on start when missing
S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY required Storage credentials
S3_FORCE_PATH_STYLE true false for AWS S3 and Tigris; true for most others
APP_URL http://localhost:3000 The public origin: links, CORS for uploads, and where cookie-signed writes are taken from, beside organizations’ verified domains
INTERNAL_URL APP_URL Where the server reaches its own API while rendering pages, when it can’t reach APP_URL from inside (a container behind a proxy): http://localhost:3000
CHROMIUM_PATH unset A Chromium the server runs headless to draw brand pages as pictures (preview_page over MCP): /usr/bin/chromium-browser in the Docker image. Unset: the machine’s Google Chrome if there is one; otherwise previews say they can’t be drawn
BILLING_URL unset Where an organization’s admins manage the plan behind its limits: a “Manage plan” link in Settings, Usage, an “Upgrade” button in the header while the organization has no limits of its own, and named when a limit refuses something. See Limits
GIT_CONNECT_URL unset Where a brand gets connected to a Git repository by this server’s Git integration, {brand} standing for its slug, left empty to bring a new brand in from a repository. New brand, the brand’s menu (Connect Git) and Edit’s Git way link there, for project admins. See Sync API
FIX_ISSUE_URL unset Where a fix becomes an issue in this server’s tracker: Fixes links each place still loading an old version, and each search that found nothing, to it as Open an issue, with kind (stale or gap) and place or q in the query. The tracker reads the fix itself (GET /api/v1/fixes, as the person) and files it. Unset: no such link
SITE_BUILD_URL unset Where a built site gets connected to a repository by this server’s build integration, {site} standing for its address: an integration that builds it on each push and previews pull requests. A site’s Deployments link there, as Build from GitHub. Unset: a site is deployed with site push, a zip, or a deploy hook
EDGE_SECRET unset Shared with an edge in front of the server that serves public builds from storage itself: it asks GET /api/v1/sites/edge?host= with it in X-Edge-Secret. 32 characters at least. Unset: that endpoint answers 404. See Sites
ASSISTANT_URL unset An assistant this server offers beside the app: the address of its endpoints, on APP_URL’s origin, which answer questions about the library as the person (through the API or MCP). Ask in the sidebar, and Ask about it on an open asset, open the app’s panel, which calls them: GET {url} (the panel’s start), GET {url}/chats/{id}, and POST {url}/ask (the answer, as JSON lines). Unset: no Ask
FEATURES_FILE unset The server’s own features, beside the core’s, for admins to turn on and off in Settings, Features: a JSON or YAML file of { features: [...] }, each with id, name, description, where it is switched (levels: organization, project, or both), its default, whether it takes a plan (paid: off without one, with the way to a plan in Settings), what Settings says before it is turned on or off (confirm: on, off), the key and fields it needs in a project, the hook told when it is turned on or off (url, and secret: the name of the variable holding the signing secret), its manage view (the address of what Settings shows under Manage, GET {manage} and POST {manage}/actions/{id}, drawn by the app), and what of the server’s it gates (assistant, brandImport). See decision 0018. Unset: the core’s own only
FEATURES unset FEATURES_FILE’s text itself, JSON or YAML, for a host whose settings are variables rather than files (a fly.toml’s [env], Render, Kubernetes). Set one of the two, not both
BRAND_IMPORT_URL unset A brand importer this server offers: the address of its endpoints, on APP_URL’s origin, which read everything a brand has into a plan the person reviews, through the API as the person. New brand offers it as a fourth way to start, Import your brand: a .zip of the brand’s files (its folders become collections, a brand book in it is read into rules and draft guideline pages, and logos can be cut from the book) or the guidelines as a PDF. Nothing is added until someone applies the plan, and an applied import can be undone. The app draws it: GET {url} (what may be imported), POST {url} and PUT {url}/{id}/parts/{n} (the upload, in parts), POST {url}/{id}/complete (read it), then the import’s page. Unset: no such choice
DOMAIN_TARGET unset The host name organizations’ own domains should CNAME to, shown beside their TXT record: this server’s, or a provider’s (Cloudflare for SaaS’s fallback origin). Unset: “point it at this server”
PORTAL_DOMAIN unset A domain whose subdomains are portals: {address}.PORTAL_DOMAIN serves that portal, beside /p/{address}. Needs a wildcard DNS record and certificate. See Portals
BUILD_DOMAIN unset A domain of their own for built sites: {address}.BUILD_DOMAIN serves a site’s builds and {name}--{address}.BUILD_DOMAIN its previews, while PORTAL_DOMAIN keeps the portals’ pages. A build runs its owner’s JavaScript: on another registrable domain it can’t set cookies the portals read. Needs a wildcard DNS record and certificate. See Sites
HUB_URL unset Where BrandHub answers: a host of its own (https://hub.example.com) or a path on APP_URL’s (http://localhost:3000/hub). The path is always /hub. A host beside APP_URL’s (hub. and app.example.com) shares its sign-in: the session cookie is then set for example.com. Unset: no hub. See Portals
COOKIE_PREFIX better-auth The start of the session cookie’s name. Set it when another install’s cookie reaches this one, as a hub’s does for its whole domain (staging.example.com beside app.example.com): two cookies of one name, and the other’s would win. Lowercase letters, digits and dashes
BETTER_AUTH_SECRET required in production Signs sessions, encrypts secrets in settings. openssl rand -base64 32
SIGNUP invite open: anyone may make an account, and gets an organization of their own
SETUP_TOKEN unset Asked for by the first account, the admin of everything, so whoever reaches a fresh server first can’t make it. Unset: anyone can, and the log warns at start until someone has
OIDC_ISSUER OIDC_CLIENT_ID OIDC_CLIENT_SECRET unset Single sign-on; all three or none. See Authentication
OIDC_NAME SSO The button: “Sign in with OIDC_NAME”
GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET unset Continue with Google on the sign-in and sign-up pages, from a Google Cloud OAuth client whose redirect URI is {APP_URL}/api/auth/callback/google; both or neither. Google vouches for the address only: who may make an account still follows SIGNUP
TURNSTILE_SITE_KEY TURNSTILE_SECRET_KEY unset Cloudflare Turnstile on making an account with a password at APP_URL, a proof of work at an organization’s own domain; both or neither. See Authentication
SIGNUP_CAPTCHA unset pow: a built-in proof of work on making an account with a password, on every host Turnstile doesn’t cover. See Authentication
EMAIL_PROVIDER unset resend, postmark, sendgrid or console. Set: every organization’s email, and new accounts confirm their address with a code. Unset: no email, unless an organization sets its own
EMAIL_FROM EMAIL_REPLY_TO EMAIL_API_KEY unset Sender, reply address, provider key. EMAIL_REPLY_TO is also where Help’s Send feedback writes; unset, the app offers none
TERMS_URL PRIVACY_URL unset Your terms and privacy policy. Set: wherever an account is made, it says that going on agrees to them, with links. PRIVACY_URL also shows as a “Privacy” link where visitors without an account land: portals (their door and requests for access too), share links and BrandHub
BRAND_NAME BRAND_TAGLINE BRAND_ACCENT the product’s The server’s brand, under every organization’s own. See White-labeling
ANONYMOUS_SCOPE nothing What a request with no key and no session may do: read makes a public library
TRUSTED_PROXIES unset The reverse proxies in front, whose X-Forwarded-For the server believes: addresses, ranges, or private. Unset, the header is ignored and every client shares one rate limit. See Security
RATE_LIMIT 1200 /api requests per minute per client; 0 turns it off. See Security
LIMIT_STORAGE LIMIT_EDITORS LIMIT_PROJECTS LIMIT_BRANDS LIMIT_DOMAINS LIMIT_EMAILS LIMIT_FEATURES LIMIT_SITE_REVIEW unlimited Every organization’s limits. See Limits
SITE_REVIEW_EMAIL unset Where a built site held for review is reported, through the server’s email. See Held for review
LIMIT_ORGANIZATIONS unlimited Per person: how many organizations on the server’s limits (no row of their own) someone may be admin of and still make another. See Limits

Some configuration is also a setting an organization can change for itself in the app (Settings), such as its email. A value resolves property by property from the narrowest place that says something: the project, then the organization, then the environment, then the default. So a server configured once serves every organization, and one of them can change what the server leaves open to it. Email is an exception: once EMAIL_* is set, it is the server’s alone, and no organization overrides it. GET /api/v1/settings?context=organization says where each value comes from.

Limits are the exception: an organization can read them, never change them.