Configure
Env vars, custom domains, rate limits, collaborators, source edits, and storage for one app.
All of this lives in the app’s Settings page (/apps/<id>/settings), the item directly under Pull requests in the sidebar. Every section is stacked on that one page, each with its own heading and save flow: Identity, Domains, Limits, Error reporting, Collaborators and Environment, then Branch protection and Danger zone for app admins. ?section= is the deep link — so Settings → Domains is /apps/<id>/settings?section=domains, which opens scrolled to the Domains heading. There is no in-page section nav and no app-level Settings tab; the app’s own tabs (Overview / Deployments / Metrics / Errors / Events) carry no Settings item. Old ?panel=settings links redirect to the page and keep their ?section=.
Environment variables
Settings → Environment. Variables reach the install, the build and the release command, so a bundler can inline them (for example Vite’s import.meta.env), and every variable also becomes a Worker vars entry the deployed code reads as env.NAME — a setting wins over the wrangler.jsonc’s own value for that name. A change applies from the next deploy; redeploy to apply it now. Values are hidden after saving; reads are view-gated, writes admin-gated. Download mirrors the .dev.vars file local dev expects. Names starting with FLAG_ render as on/off toggles (1/0).
Reserved names come in two sets. For the install/build/release/fleet process environment the reserved prefixes are dropped silently: PORT, HOST, AWS_*, S3_*, CELLD_*, RUNNER_*, NOITE_*, BETTER_AUTH_*, CADDY_*, LD_*, NODE_OPTIONS, BUN_* (except the cache var), VOID_PROXY_*. For Worker vars only the runner-owned names are refused — and loudly: a setting or a config vars entry named __VOID_PROXY, __PROJECT_ID, __VOID_PROXY_TOKEN, __VOID_PROJECT_SLUG, __VOID_EMAIL_DOMAIN, __VOID_EMAIL_DEPLOYMENT_ID, __VOID_SANDBOX, __VOID_SANDBOX_ADMISSION, __VOID_SANDBOX_ADMISSION_NAME, __VOID_SANDBOX_MAX_CONCURRENT or __VOID_SANDBOX_SOURCE fails the deploy naming it. Every other name, BETTER_AUTH_* included, deploys as a Worker var. Full detail: Environment variables.
Void apps (built with the Void framework) get runner-owned additions at deploy time. The deploy injects identity vars — __PROJECT_ID (the app id), __VOID_PROJECT_SLUG (the slug) and __VOID_PROXY_TOKEN (a random per-fleet token, minted once and stored as a setting), plus __VOID_EMAIL_DOMAIN when the operator configured a sender domain — the __VOID_PROXY service binding to the platform’s cohosted proxy script, and the ISR_CACHE KV namespace each fleet gets its own of, so a preview never shares production’s ISR cache. That makes ISR_CACHE and the whole VOID_PROXY_* namespace reserved Worker-var names on Void apps only, and a Worker config named noite-void-proxy or noite-void-sandbox fails the deploy (those names belong to the platform’s Void proxy and sandbox controller scripts). A sandbox app also gets the __VOID_SANDBOX service binding to that controller, and a secrets.required entry naming it is satisfied by the runner like the other provisioned names. When a fleet has no BETTER_AUTH_SECRET value, the deploy generates one and stores it as a setting — Settings → Environment for production, the environment’s own Variable overrides for a preview (each fleet gets its own secret; a preview deploy never touches the app row) — set your own value to replace it. Previews sharing an external DATABASE_URL share sessions regardless. Provisioned settings list like any other (values hidden as usual) and never reach the build, release or fleet process env; deleting one just re-mints it on the next deploy.
Void apps send email and call AI through that proxy (revalidate(), sendEmail(), ai.* in the Void runtime), which each fleet carries beside the app. Three Settings names feed the proxy alone — VOID_PROXY_SMTP_USERNAME, VOID_PROXY_SMTP_PASSWORD (your SMTP relay login for this app) and VOID_PROXY_AI_API_KEY (your key for the operator’s AI endpoint, when it needs one). They list and redact like any other setting, inherit into previews unless overridden there, take effect on the next deploy — and they never reach the build, the fleet, or Worker code: the deploy splits them out before the app merges. The relay host, sender domain and admission rules are operator config (Void email and AI); unset, email sends and AI calls answer 501 while ISR revalidation keeps working.
Custom domains
Settings → Domains. Adding a hostname claims it for the app; proving
ownership takes one DNS step: publish the TXT record the panel shows
(_noite-challenge.<hostname> containing the claim’s token — the same shape
as ACME’s _acme-challenge DNS proof, under our own label), then press
Verify. Only a verified hostname routes, resolves and gets a certificate
on the first visit (ask-gated on-demand TLS), picked up on the next reconcile
(seconds). Then point DNS at the server:
dig +short app.<domain>
Validation rules: lowercase DNS shape, ≥ 2 labels, no wildcard/IP/port/path.
Platform hostnames are refused. Several apps may hold a pending claim for one
hostname, but the first to verify wins: a hostname another app already
verified answers 409, and verifying drops rival pending claims. In single
tenancy (one trusted operator) a claim verifies immediately, with no TXT
step. Removing a hostname asks first, then releases it: the edge drops the
route and the hostname’s certificate.
Rate limits
Settings → Limits (app admins). Requests per minute the edge lets through to this app, across all its hostnames: per visitor (client IP) and for the whole app. Leave a field empty for the platform default or enter 0 for no limit; the edge applies a change within seconds. Past a limit, visitors get 429 Too Many Requests with Retry-After. Behind a proxy the edge does not trust, per-visitor limits are off and the field says so. See Edge protection.
Collaborators
Settings → Collaborators. Roles, weakest first:
| Role | Can |
|---|---|
view |
Read the app, logs, metrics, storage |
push |
Everything above, plus push code (create + fast-forward only) |
admin |
Everything above, plus settings, env vars, domains, any push, collaborator management |
Removing a collaborator asks first, then drops the grant and any invitation addressed to them: the account loses access immediately. Instance admins (the operator track) manage every app without a grant.
Source browser and web commits
While you are on an app, the sidebar lists four items under it: App (this page), Code, Pull requests (Pull requests) and Settings (the sections below). Code opens /apps/<id>/source: the files of the default branch, deep-linkable through ?ref=, ?file= and ?panel=. Editing files there stages a web commit, which pushes like a deploy and wakes a sleeping app. The same page browses any branch, tag or commit, shows history and diffs, and compares refs; see Source and history.
Storage
The Overview tab lists the app’s storage resources; each row opens a detail browser. Kinds: D1 databases, Durable Objects, R2 buckets — as declared by the deployed Wrangler config (read first) or the pushed source. R2 keys browse by prefix.
D1 table editor
A D1 database opens a full-height table editor. The left rail lists every table with its row count (and a lock on tables the caller may not write), with its own table search; on small screens it collapses into a table picker above the grid.
The toolbar searches rows server-side, and offers a filter popover (column / operator / value, AND-ed, up to 10), a sort picker (clicking a column header cycles ascending → descending → off), refresh, and Insert. Filter, sort, page and page size are URL state (?table=, ?page=, ?size=, ?sort=col:asc, ?f=, ?q=, ?view=), so a view is deep-linkable.
The grid has a sticky header (primary-key icon, column name, declared type, sort indicator); a NULL cell renders as a NULL badge and an empty string as a faint "", and long values truncate with the full value in the tooltip plus a hover copy button. Timestamp-looking columns show the raw value with a local-time hint. Tables that allow deletes get a checkbox column with a bulk-delete bar.
Clicking a row opens the right-side row editor: every field stays raw text (never a typed date/number input, so a stored value survives an untouched round-trip), NULL is an explicit toggle, and long, multiline or JSON-looking values get an auto-growing textarea. Helpers never reformat a value the user did not touch: Now writes in the field’s existing format, Format JSON pretty-prints valid JSON (and flags invalid JSON), a boolean column gets a 0/1 toggle, and a foreign key gets an Open → jump to the referenced table filtered to that value. Changed fields are highlighted, the footer counts the changes, and only the changed columns travel — an untouched insert field is omitted so its DDL default applies. Delete asks inline and shows the key.
Footer paging (page input, prev/next, 25/50/100 per page, record count) sits next to a Data | Definition switch; Definition shows the columns, indexes, foreign keys and the CREATE TABLE statement with a copy button.
The control plane’s own database (the _control admin home) uses the same editor: capabilities come from its write policy per table, secret columns render masked (•••• redacted) and are never filtered, sorted or written, and rows of user/invite link to the Users/Invites tab that owns them. A view-role collaborator sees the same editor with no write controls.