Setup Wizard
Purpose. First-boot configuration in the browser for people who would rather not write an answers file. Opening http://<hostname>/ (or http://<ip>/ before DNS resolves) on a server whose setup is still pending shows the wizard — plain HTTP, because the bootstrap Caddyfile has auto_https off and HTTPS starts only once setup applies; nothing else is reachable until it finishes.
Behaviour.
-
Seven steps: hostname → TLS → first domain → owner → AI (optional) → apply → DNS records. Each of the first four is exactly one call to the setup endpoints (
PUT /setup/hostname,PUT /setup/tls,POST /setup/domain,POST /setup/owner) — except that the TLS screen also carries the POP3 switch, recorded by a second call,PUT /setup/protocols, which is not a step: leaving it alone keeps POP3 on, andGET /setupreports it aspop3; apply isPOST /setup/complete, which renders and validates the daemon configuration and rolls back if any daemon rejects it. The wizard keeps no state of its own: reloading the page resumes at the first step the API has not recorded (GET /setup), and the result is byte-for-byte the same asmailctl install --answers(Headless install (answers file)). -
TLS modes: automatic (Let’s Encrypt, needs a contact email and ports 80/443 reachable), bring-your-own (paste the PEM chain and key), or none. None is for a stack whose mail ports are published on loopback only — a dev stack, CI: with no TLS the only way a client can sign in to IMAP and POP3 is by sending its password in the clear, and a server doing that on the internet is what CERT-Bund scans for and reports to the hosting provider.
GET /setupreportspublicBind— whetherPUBLISH_BINDis anything but loopback — and when it is,PUT /setup/tlsrefuses{ mode: "none" }with400 plaintext_auth_exposedunless the body carriesallowPlaintextAuth: true. The wizard shows the escape as a checkbox the person must tick, and from then on the apply step, Settings andmailctl doctor(a ✖) say the server takes passwords in clear text on 143 and 587, and on 110 while POP3 is on, for as long as the mode is in use. -
The owner step pre-fills
admin@<first domain>and, by default, also creates that mailbox. -
AI is optional and skipped by default. The step renders the served catalogue (
GET /setup/ai/providers) — never a list compiled into the SPA — takes a model, an endpoint and a key, offerslocalOnly, and offers the connection test once a configuration is saved. Reading the catalogue isGET /setup/ai/providers, reading the stored configurationGET /setup/ai, writingPUT /setup/ai, testingPOST /setup/ai/test, undoingDELETE /setup/ai; all five are unauthenticated while setup ispendingand answer 409 afterwards, exactly like the steps above, and they read and write the same per-tenantai_provider_configthe admin AI page uses (AI providers). Skip for now writes nothing at all, which leaves AI off — the state every install ships in.localOnlyis enforced at write time here as everywhere else: the endpoint is resolved and refused if any address it answers with is public. -
The picker itself is one component (
apps/admin/src/components/ai-provider-form.tsx), shared with the admin AI page rather than reimplemented, so the catalogue, the local-only rule and the “an empty key field keeps the stored key” rule cannot drift between the two screens. -
The final step lists the DNS records to publish, and the list depends on the configuration that was just applied.
SetupService.complete()callsrequiredDnsRecords(…, dnsRecordOptions(applied.config, { serverIp6: hostIpv6(cfg) }))— so while IPv6 is on it also lists the AAAA and the IPv6 PTR — rather than passing no options, so the wizard shows the same conditional set the domain page does — the alternative is the silent-smaller-answer failurednsRecordOptionsexists to prevent, which once had the wizard configure a relay and then print an SPF record that ignored it. Always: A and PTR for the mail host (whenPUBLIC_IPis known), MX, SPF for the domain, DKIM — the first domain’s key is generated during apply — DMARC, the_imaps/_submissionSRV rows, and a second SPF for the mail host itself (forwarded mail leaves asSRS0=…@<hostname>). On an ACME install it also carriesautoconfigandautodiscoverCNAMEs — there is nowebmailrow, because the mail client is one host for the whole server,webmail.<the server's own domain>, published once by hand (Install step 7) — and — unless MTA-STS isnone— the_mta-stsTXT, themta-stsCNAME and the_smtp._tlsTLS-RPT TXT. If a relay is configured, its SPF term is folded into both SPF rows (v=spf1 mx include:… -allfor the domain,v=spf1 a include:… -allfor the mail host), independently of the TLS mode; a relay that declares no term instead makes those rows carry a ⚠ warning that the record as printed hard-fails the server’s own mail. Copy buttons throughout; Sign in and verify DNS then lands on the domain page after login, where Verify now checks the records live (DNS records & live verification). -
Once
state = completethe wizard route redirects to/login; the setup endpoints answer 409 (setup is one-shot), andGET /setupanswers only{ "state": "complete" }. The final step keeps what it read while setup was pending, which is where its sign-in link onhttps://<hostname>comes from. -
The wizard is exercised in CI, in a browser, on every run.
apps/admin/e2e/setup/wizard.spec.tswalks all seven steps against the served SPA and is what performs first boot for the wholesmokejob — the API smoke’s own headless setup is conditional onsetup=pendingand finds the work already done. It runs as its own Playwright project (pnpm e2e:setup) before the smoke, because a stack can only be set up once and whoever goes first is the only one who sees a wizard.
Configuration. None. The wizard is part of the web image and is served by Caddy; the API’s /api/v1/health tells the SPA whether setup is pending.
Limits & edge cases. The wizard’s DNS step is a checklist (there is no session yet); verification runs in the domain’s DNS & routing section, where Sign in and verify DNS lands after sign-in. GET /setup has no flag for the AI step because nothing in it is required, so a reload lands on that step rather than past it — it costs a click of Skip for now, where skipping over it would hide the only place the wizard offers AI at all. Per-domain AI policy and each mailbox’s opt-in are not offered here: they live in the admin UI and belong to people who do not exist yet at first boot. If the browser tab is closed mid-way, just reopen / — progress is on the server.