MAIL_TRANSPORT was a fallback left from the old registration flow that sent
confirmation mail. That flow is gone; the variable outlived it.
It was never the primary source anyway. getTransport reads server_config
('server-settings' → smtp) first, which already backs a full UI at Settings →
Server → SMTP and its API in api/server-settings/smtp.ts, supporting resend,
smtp and mailhog. The env var only answered when that was absent — which is a
second source of truth for something the owner can already set, with the failure
mode that a stale URL in .env silently answers for a server whose settings row
is simply empty.
Removed from transport.ts, .env.example and the setup script's Environment
section, which no longer asks for it. setup-old/ still mentions it; that is the
archive and is left alone.
Also split the try. It wrapped the read AND the transport construction and
swallowed both, so three different problems produced one message. Unreachable
database, nothing configured, and stored settings that do not build a transport
now say different things, because the fix for each is different and this message
is all the caller ever sees.
The two consumers — queue/engine.ts and auth/forgot-password.ts — now raise
until SMTP is set in the UI, which is the honest answer rather than a regression.
Not typechecked: node_modules is empty here and installs are frozen. transport.ts
parses under `bun build --no-bundle`; the setup script was run and no longer
prompts for or writes the variable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writes .env, and the whole point of the section is the two values it must not
write twice.
JWT_SECRET and VAULT_STORE_KEY are read back from any existing .env and kept.
The original script reminted JWT_SECRET on every run that agreed to regenerate
.env, which logs every device out with no stated reason, and never wrote
VAULT_STORE_KEY at all — so a scripted install had no at-rest key and the vault
and wallet refused to store anything.
VAULT_STORE_KEY is the more dangerous of the two now that it is being written.
It is not Vaultwarden's despite the name: it encrypts every secret column in
Postgres, and the wallet seed envelope on top of the owner passphrase. Changing
it is unrecoverable for the seed, because the passphrase opens the inner
envelope and that is the outer one. Said in the section, in the file it writes,
and in .env.example, which described it as Vaultwarden's and understated it.
DATA_PATH and OFFICER_ITEMS_DIR are derived from $OFFICER_ROOT rather than
asked — two questions that had to agree with each other and with the app store.
ALLOW_ANY_ORIGIN is written explicitly from whether tailscale0 exists, rather
than left to the platform default. The default is ON, which CLAUDE.md says is
only defensible because the tailnet is the perimeter; with no tailnet there is
no perimeter, so it goes out as false. Added to .env.example, which omitted it.
PORT defaults to 9000, matching .env.example. The old script used 9010; nothing
depends on either, and it is a prompt.
write_env restores the prior umask. It was set to 077 so the secrets are never
briefly world-readable, but umask is not scoped to a function and would have
made every file the later sections create owner-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>