Preflight and Instance status
The checks before setup, and the staff page after it.
Two pages say whether an installation can run, and both run the same checks (src/lib/instance/checks.ts):
- Preflight (
/preflight), which anyone can open until first-run setup is done. - Instance status (
/instance/status), which only staff can open, once setup is done.
Preflight
Until setup is complete, Preflight is where the setup link leads: opening the link unlocks setup in that browser and shows Preflight first, and Continue to setup goes on to the steps.
- It is behind the setup link. While setup is open, Preflight is part of setup: without the link,
/preflight, like/setup,/and/login, shows only where to find it. The one exception is a database that cannot say whether setup is done; then anyone sees the checks, since that is when they are needed and there is nothing yet to protect. - It shows no detail. Each check's status and a fix that reads the same on every installation, but never a secret, a hostname, an address, a bucket name or an error message. The startup log and Instance status have those.
- Errors block setup, warnings don't. An error is something setup cannot work without, and Continue to setup stays disabled while there is one. A warning doesn't stop setup, but fix it before people rely on the installation.
- Run the checks again after each fix. A change to
.envneeds a restart first. - After setup the page shows no checks. Staff are sent to Instance status, everyone else to sign-in.
The setup link
Setup has no form to type a code into. It opens only through its link, such as https://support.example.com/setup#token=Zq8vK2pL5mN0rT3wY6bC9fH1jD4gS7aX0eU2iO5kQ8z, which bin/install prints and offers to copy, and which the startup log repeats on its [setup] Setup link line. The installer copies to the local clipboard when it runs on your own machine, and over SSH to the clipboard of the terminal you are using (OSC 52, which iTerm2, kitty, WezTerm, Ghostty, Windows Terminal and others accept, and macOS Terminal does not).
- Nobody can guess it. The token is 256 random bits, generated once and stored, so every replica prints the same link. Anyone who opens
/setupwithout it finds only where to look for it, and the page's own attempt limit still applies to every token posted. - It never reaches a log. The token travels in the fragment, the part after
#, which a browser never sends: no server log, proxy access log orRefererever holds it, and a chat app's link preview cannot see it. The page takes it out of the address bar as it loads, and posts it once with its own form (src/components/setup/setup-link-code.tsx); a POST body is not logged. - It works until setup is done. Then the token is cleared and the link opens nothing. Until then, whoever holds the link can set up the installation, so keep it to yourself; a clipboard manager may keep a copy that long too.
- A fixed one comes from
SETUP_TOKENin.env, for installs that script the whole of it: the link isAPP_URL/setup#token=followed by its value. Production ignores aSETUP_TOKENweaker than about 60 bits, and generates a token instead.
While the database can't be reached, or has never been migrated, the root and /login fail as they always have, because they can't tell whether setup is done. Open APP_URL/preflight yourself: it shows its checks whenever it can't tell.
What it checks
Each check has 5 seconds, and they run side by side, so the page answers within a few seconds whatever hangs.
| Check | Error | Warning | Fix |
|---|---|---|---|
| Configuration | The environment is refused: a required variable is missing or invalid, or, in production, a secret is too weak. | The startup log's [config] lines name each variable. Correct .env and restart. | |
| Secrets | BETTER_AUTH_SECRET or PORTAL_LINK_SECRET is unset or shorter than 16 characters. In production, either of them, or a key in SECRET_ENCRYPTION_KEY, is the .env.example value, one repeated character, or shorter than 32 characters. | The same weak values in development, which runs with them. In production, SECRET_ENCRYPTION_KEY unset. | openssl rand -base64 32 for each. For SECRET_ENCRYPTION_KEY, read Encryption at rest first. |
| Public address | The page was opened at a host other than APP_URL's, where sign-in and setup refuse to work. | The request's scheme differs from APP_URL's. A path in APP_URL. In production, APP_URL unset, an address on this machine (such as localhost or 127.0.0.1), or plain http. | Open the address in APP_URL, or set APP_URL to the one people will use, and restart. |
| Database | SELECT 1 fails, or has no answer. | Check DATABASE_URL, and that PostgreSQL accepts connections from this server. | |
| Database migrations | _prisma_migrations doesn't exist, a folder in prisma/migrations hasn't been applied, or a migration failed part-way. | The database has a migration this version doesn't know, so a newer version migrated it. | prisma migrate deploy. Compose runs it before the web container starts. |
| Attachment storage | The bucket refuses the request or doesn't answer. A bucket that doesn't exist yet is fine: the first attachment creates it. | Check S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY. | |
| Outbound DNS | api.resend.com can't be looked up. This is a DNS lookup only: no check calls Resend or Cloudflare. | Allow outbound DNS from the server and its container (/etc/resolv.conf, Docker's dns setting). |
How the address check reads a request:
- Host: the
Hostheader, which is also what decides where a request may go. A reverse proxy has to pass it through. - Scheme: from
X-Forwarded-Proto, readTRUSTED_PROXIEShops from the right, the wayX-Forwarded-Foris read. WithTRUSTED_PROXIES=0no forwarding header is believed, and the scheme is plain http, which is all Bouée's own server speaks. - So an SSH tunnel or the server's IP fails it, as setup itself would.
- A scheme that differs is only a warning. Next reports plain http whenever a proxy sends no
X-Forwarded-Proto, TLS or not, and withTRUSTED_PROXIES=0nothing a proxy says is believed. Neither stops sign-in when the browser is really on https, so neither blocks setup. The warning says which of the two it is, and that on plain http the browser won't keep the sign-in cookie.
The warnings about APP_URL and SECRET_ENCRYPTION_KEY come from the same rules as the startup log's [config] WARNING lines, so the two always agree, development included.
Instance status
APP_URL/instance/status is for staff, the people who administer the installation (User.role is OWNER). /instance opens it too. It is guarded as Instance settings is: a visitor with no session is sent to sign-in and brought back, and an agent who isn't staff goes to their workspaces. It answers only on APP_URL's host.
It shows:
-
Installation:
- the edition, open source (FOSS) or Enterprise Edition (EE);
- the version from
package.json, and the build ID Next gave this build; - the Node.js and PostgreSQL versions;
- how long the server has been up;
- which server process answered, by hostname and process ID.
-
License, in the Enterprise Edition: its status, customer, plan and expiry, where the key comes from, and a link to Instance settings, where it is managed.
-
Checks: every Preflight check, with the detail Preflight leaves out, such as hostnames, addresses and error text. Secret values are never shown: credentials written into a URL, and the value of any secret variable, are taken out of error text.
-
Background jobs: each sweep the server schedules at startup:
- incoming and outgoing mail retries;
- hostname checks;
- audit log and notification retention;
- auto-close.
For each one, the page shows how often it runs, when it last ran and for how long, how many runs failed, and its last error. The server process keeps these numbers in memory, so they cover only the process that answered: another replica keeps its own, and a restart starts from zero.