docs/env-vars.md
Rama main · réplica de lectura del repositorio original.
# Environment variables
Runtime secrets are configured as **Bunny native secrets** in the Bunny Edge
Scripting dashboard. They are read at runtime via `process.env`. This page is
the reference for every variable the site and the build read. AGENTS.md keeps
only the three that every boot needs.
The optional static CDN is different: `CDN_URL`, `CDN_BUNNY_STORAGE_ZONE_NAME`,
`CDN_BUNNY_STORAGE_ZONE_KEY`, `CDN_BUNNY_STORAGE_HOST`, and
`CDN_BUNNY_PULL_ZONE_ID` are GitHub repository secrets used only while building.
When all five are set, the build uploads site-independent browser assets and
image-codec WASM under an immutable content-addressed path, purges the pull zone
with the existing `BUNNY_ACCESS_KEY` repository secret, verifies every public
object byte-for-byte, then bakes those public URLs and their CSP origin into the
edge script. They must not be added to the running Bunny script. With all five
absent, assets stay embedded; a partial set fails the build. Site-bound assets
such as `embed.js` and the dynamic `/order.js` body remain in each script. Use
the Storage API hostname shown on Bunny's Storage **Access** page for
`CDN_BUNNY_STORAGE_HOST` (for example, `storage.bunnycdn.com` or
`uk.storage.bunnycdn.com`).
## Required (configure in Bunny dashboard)
- `DB_URL` - Database URL (required, for example `libsql://your-db.turso.io`)
- `DB_TOKEN` - Database auth token (required for remote databases)
- `DB_ENCRYPTION_KEY` - 32-byte base64-encoded encryption key (required)
## Optional
- `PORT` - Server port (defaults to 3000, local dev only)
- `BUNNY_API_KEY` - Bunny API key (required for custom domain management, with
`BUNNY_SCRIPT_ID`)
- `BUNNY_SCRIPT_ID` - Bunny Edge Script ID (required for custom domain
management, with `BUNNY_API_KEY`)
- `STORAGE_ZONE_NAME` - Bunny CDN storage zone name (required for image uploads)
- `STORAGE_ZONE_KEY` - Bunny CDN storage zone access key (required for image
uploads)
- `BACKUP_PAGE_SIZE` - Rows read per keyset page when dumping a table for backup
(default 500). Each page is one libsql response, so this bounds the response
size to stay under libsqld's "Response is too large" payload cap. Used by
`deno task backup` and the admin Backups page; migrations no longer back up
inline (the edge subrequest budget cannot fit a full dump), so backups are
taken out-of-band.
- `MAIN_INSTANCE_KEY` - Shared secret authorizing the inter-instance
site-credentials endpoint (`POST /instance/site-credentials`). When set on a
builder/main instance, that endpoint returns built sites' DB URL + token to a
caller presenting this key as a bearer token, so the upgrade workflow can back
each site up to the builder's storage before deploying. The returned token is
each site's own full-access credential (the same one the site runs with) —
callers only read, but must treat the response as write-capable production
secrets. The caller passes the release tier it is publishing as
`?tier=alpha|beta|release` (a tier-less call defaults to `release` ⇒ the whole
fleet, which is what the single-site `backup-site` action relies on). Each
site carries an `updates` channel and only the sites at that tier or more
eager are returned (a `release` deploy reaches every site, `beta` reaches
beta + alpha sites, `alpha` only alpha sites — an unknown tier is a 400). The
response echoes the applied `tier` so a caller can confirm the server actually
filtered: a pre-tier build ignores the query string and omits it. The
canary workflow then fails closed and does not send a non-release deploy to
the whole fleet. Unset `MAIN_INSTANCE_KEY` ⇒ the endpoint is disabled (404). A
disabled endpoint reports nothing, so a stranger cannot make a non-builder
site send notifications. A configured endpoint reports every call through the
error path. The code is `E_INSTANCE_CREDENTIALS_REFUSED` when the bearer token
is absent or wrong. The code is `E_INSTANCE_CREDENTIALS_ACCEPTED` when the key
matches. An accepted report names the tier and the number of sites returned,
or it says that the caller named an unknown tier.
No report carries the key, the bearer token, a database URL, or a database
token. The upgrade workflows read the key from the `MAIN_INSTANCE_KEY`
repository secret. Do not pass it as a run input: GitHub writes run inputs to
the public job log.
- `DENO_DEPLOY_TOKEN` - Deno Deploy organization access token. Required with
`DENO_DEPLOY_ORG_ID` and `DENO_DEPLOY_ORG_SLUG` to build sites on Deno Deploy.
- `UPDATE_IN_PROGRESS` - Update write-stop. The deploy workflows set this
variable on the site's edge script before they update the site's code, and
they remove it after the new build serves its first request. While it is
set, the app refuses every write, the read-only page shows the update
message, and a webhook answer is a 503, so the sender redelivers it. A
failed deploy leaves the variable set. Re-run the same update to clear it,
or remove the variable by hand in the Bunny dashboard under the script's
Environment Secrets.
- `DENO_DEPLOY_ORG_ID` - Deno Deploy organization ID used by the app creation
API.
- `DENO_DEPLOY_ORG_SLUG` - Deno Deploy organization slug used in each app's
managed `<app>.<organization>.deno.net` production domain.
- `OPENCODE_API_KEY` - OpenCode zen API key for the `deno task grade:code`
command. Jev, the grader's judgment engine, sends this key to a paid endpoint.
The dev shell exports the value of `/run/secrets/opencode_api_key` when the
environment does not already set the variable. Without a key, the grader runs
the mechanical checks and skips Jev.
- `BUNNY_DNS_ZONE_ID` - Bunny DNS zone ID for subdomain registration (enables
subdomain feature when set with `BUNNY_API_KEY`)
- `BUNNY_DNS_SUBDOMAIN_SUFFIX` - Suffix appended to user-chosen subdomain (for
example `.tickets`)
- `NTFY_URL` - Ntfy endpoint URL for error notifications (for example
`https://ntfy.sh/your-topic`). Sends domain and error code only, no personal
or encrypted data. A call to `POST /instance/site-credentials` also sends its
error code only. Its tier and site count stay in the console line, in Sentry,
and in the encrypted activity log.
- `SENTRY_URL` - Sentry DSN for server-side error reporting (for example a
self-hosted Bugsink: `https://<key>@bugs.example.com/<project>`). When set,
the same classified server errors that log to the console and ping ntfy are
also captured by Sentry, with a real stack trace when the originating
exception is available. Unset ⇒ Sentry is disabled (the SDK never
initializes). The release is `chobble-tickets@<commit>`, matching the source
maps the deploy workflows upload; readable (un-minified) traces additionally
require the `SENTRY_AUTH_TOKEN`, `SENTRY_CLI_URL` (the instance base URL, for
example `https://bugs.example.com/`), `SENTRY_ORG`, and `SENTRY_PROJECT`
GitHub Actions secrets so the deploy can inject debug IDs and upload the maps.
Without those secrets the deploy still works; traces just stay minified.
- `UPTIME_KUMA_URL` - Uptime Kuma 2.4 or newer base URL used by builder
instances to inspect and add built-site scheduled maintenance monitors.
Requires `CAN_BUILD_SITES=true`, `UPTIME_KUMA_USERNAME`, and
`UPTIME_KUMA_PASSWORD`. A public host must use `https`. Cleartext `http` is
allowed only for a local network address (loopback, private, CGNAT,
link-local, or IPv6 unique-local).
- `UPTIME_KUMA_USERNAME` - Uptime Kuma username. Must be set with
`UPTIME_KUMA_URL` and `UPTIME_KUMA_PASSWORD`.
- `UPTIME_KUMA_PASSWORD` - Uptime Kuma password. Must be set with
`UPTIME_KUMA_URL` and `UPTIME_KUMA_USERNAME`.
- `UPTIME_KUMA_INTERVAL_MINUTES` - Optional positive whole number controlling
how often new built-site monitors run. Defaults to `15`.
- `CHECKOUT_WINDOW_MINUTES` - Optional positive whole number (default `60`). How
long an unpaid checkout can take payment. Square stages its cancel handle for
this window. The link expiry task ends the link at it. Stripe is told the same
expiry at creation, so the value must sit between `30` and `1440`: Stripe
refuses an expiry outside that range. A value outside it stops the server at
startup. SumUp closes its own hosted page after 30 minutes, so it reads
nothing here.
- `DEBUG_KEY` - Optional diagnostic key. `GET /health` returns a plain `Up :)`
by default; a request with a matching `X-Debug-Key` header instead returns
JSON build diagnostics (commit, build timestamp, server time) — non-private
but useful to operators. Unset ⇒ verbose health disabled. The running build
also records its commit into `settings.current_script_commit` on boot, so a
backup carries the commit the site was on and a restore can surface which
commit to redeploy (via `.github/workflows/restore-deploy.yml`).
- `SITE_BUILD_DRY_RUN` - Test environments only: `true` answers the site build
and assignment surface's Bunny and GitHub provider calls (the release fetch,
the Bunny database, the edge script and its secrets, the publish, the
assignment's renewal pushes) from a canned body instead of the network, while
the request's subrequest budget still pays for each call exactly as it pays
for a real one. A call outside the canned surface still runs for real, so an
environment with no provider credentials fails loudly there. Dry-run sites
carry synthesized values (`dry-run-N.invalid` addresses) that cannot serve
traffic. Never set this in production.
- `BOTPOISON_PUBLIC_KEY` - Optional Botpoison public key (sent to the browser).
The contact form works without it; setting it together with
`BOTPOISON_SECRET_KEY` adds proof-of-work spam protection as a progressive
enhancement. The owner still enables the form under Site → Contact and sets a
business email.
- `BOTPOISON_SECRET_KEY` - Optional Botpoison secret key. Used server-side to
verify contact form submissions when Botpoison is enabled. Never sent to the
browser.
- `ADMIN_EMAIL_ADDRESS` - Enables a superuser recovery option in owner settings.
The local-part (before `@`) must be a valid app username (letters, numbers,
hyphens, underscores). SMTP caps a local-part at 64 characters, so this one
can use 1–64 characters. Email delivery must be configured before the
superuser can be enabled. Also enables the owner-only **Support** page
(`/admin/support`), where the operator can message this address.
- `HOST_EMAIL_PROVIDER` - The host machine's email provider: `mailgun-eu`,
`mailgun-us`, `postmark`, `resend`, or `sendgrid`. Set with
`HOST_EMAIL_API_KEY` and `HOST_EMAIL_FROM_ADDRESS`. Email sends fall back to
these credentials when a site holds no email settings of its own, a site's own
settings win over them, and a builder copies each set variable to every built
site it creates, empty or not. An invalid provider logs an error. Any other
gap — a variable of the trio missing, or a From address that is not a valid
email — turns the host credentials off silently until it is filled.
- `HOST_EMAIL_API_KEY` - The provider API key for the host email credentials
(required with `HOST_EMAIL_PROVIDER`).
- `HOST_EMAIL_FROM_ADDRESS` - The From address for host email (required with
`HOST_EMAIL_PROVIDER`). Must be a valid email address.
- `SUPPORT_PAGE_TEXT` - Optional markdown shown at the top of the Support page
(requires `ADMIN_EMAIL_ADDRESS`). Use literal `\n` for line breaks since Bunny
secrets cannot hold real newlines. When unset, a placeholder note is shown
instead. The support form below it (which delivers to `ADMIN_EMAIL_ADDRESS`)
needs a business email to be set, like the public contact form.
- `SUPPORT_PAGE_MARKDOWN` - The Support page's markdown on a built site. The
Support-message tab and a fresh build's seed write this variable through the
provider API, with real line breaks. The stored text reaches the page exactly
as the editor saved it. Reads before `SUPPORT_PAGE_TEXT`, so a value saved
through the tab wins over a hand-set one.
- `SUPPORT_FORM_NAG_DAYS` - Optional positive integer (default `7`). For this
many days after a support-form submission, the Support page shows a "you last
submitted this form …" notice to discourage duplicate messages.
- `I18N_REPLACEMENTS` - Optional comma-separated `from|to` substring
replacements that rebrand the **translatable copy** of every rendered message,
for example `ticket|booking,attendee|guest`. Matching is case-insensitive and
by substring (`ticket|booking` turns `tickets` into `bookings`), and the
output copies the source word's capitalisation — `Ticket` → `Booking`,
`ticket` → `booking` (only lowercase and title-case occur in real copy). It is
applied to each message **template** once at load, and the rebranded template
is compiled and cached, so rendering stays a plain ICU format with no per-call
cost (important on a cold-booting edge runtime). It deliberately leaves alone:
HTML tags and attributes (so link `href`s survive), `<code>` examples (literal
route/CLI text), interpolated values such as a stored listing name (so "type
this exact name" confirmations still match), and the fallback key returned for
a missing translation. Avoid terms that collide with ICU keywords or
placeholder names (`name`, `count`, `plural`, …).
- `APPLE_WALLET_PASS_TYPE_ID` - Apple Wallet Pass Type ID (for example
`pass.com.example.tickets`)
- `APPLE_WALLET_TEAM_ID` - Apple Developer Team ID (for example `ABC1234567`)
- `APPLE_WALLET_SIGNING_CERT` - PEM-encoded signing certificate
- `APPLE_WALLET_SIGNING_KEY` - PEM-encoded signing private key
- `APPLE_WALLET_WWDR_CERT` - PEM-encoded Apple WWDR intermediate certificate
Apple Wallet can be configured via env vars (all 5 required) or via the admin
settings page. Admin settings (encrypted) take priority over env vars. If
neither is configured, the feature is disabled.
## Stripe Configuration
Stripe is configured via the admin settings page (`/admin/settings`), not
environment variables:
- Enter your Stripe secret key in the admin settings
- The webhook endpoint is automatically created in your Stripe account
- The webhook signing secret is stored encrypted in the database
Admin password and currency code are set through the web-based setup page at
`/setup/` and stored encrypted in the database.