John, 2026-09-19, on both counts. The old install is kept, shut down, not removed: its unit installed and disabled, its store read-only, started again if a rollback is ever wanted. When it stops being worth the disk the tool asks — keep or delete — rather than deciding, because it does not remove the thing its own rollback depends on. And the new stack depends on nothing in it. That is the shape's purpose: /opt/stalwart is a reference, everything needed is copied to new paths, and when it goes nothing notices. Nothing in the fork works against that — inbuxa.service substitutes its own prefix, no path names the old tree, and certificates and ACME keys are in the registry inside the store — so a dependency, if one appears, was made by hand during the move. Which is worth proving rather than asserting, and reversibly: nothing open under the old tree, then rename it and leave it a day under real traffic. Deleting proves the same thing and cannot be undone. Two corrections this forces. The rollback has to make the original store writable again first: the guard of step 3 blocks the Enterprise build exactly as it blocks the fork, and finding that out during a rollback is the worst time. And the copy has to be chowned — rsync -a preserves ownership, so it arrives owned by the old service user while the unit runs as User=inbuxa. Also drops the stale "untested" wording about carrying the data back. It was tested; it is impossible.
6.9 KiB
Migrating a Stalwart install to INBUXA
Status: draft, 2026-09-19. Decided in outline (John, 2026-09-19); nothing built.
Two ways in, and they are different workflows:
- Fresh installation. ihasmail-oneshot's
deploy: a new stack, new domain, nothing to preserve (SPEC.md §6.1). - Migration. An existing Stalwart server becomes an INBUXA one, keeping everything. This page is that one.
INBUXA's own cutover is the first run of it (cutover.md). The tool is what
that run leaves behind, so the second operator doesn't repeat it by hand.
What "transparent" means here
Everything that defines the server lives in its data store, so a migration that copies the store carries it all: accounts and their passwords, app passwords, OAuth clients and live sessions, aliases and mailing lists, tenants, domains, DNS records and the DNS provider settings, certificates and ACME state, Sieve scripts, the queue, and the mail itself.
Nothing is re-entered and nothing is re-issued. Users are not asked to change a password, re-authorize a client, or re-point a DNS record. That is the promise the tool makes, and the verification below is what checks it rather than assuming it.
The store format is upstream's and unchanged (SPEC.md §2.6), which is what makes this a copy rather than a conversion. A migration that also crosses an upstream version is that upgrade first, then this.
Shape
Side by side. The existing install is stopped but left standing, the fork
goes in at its own path with a copy of the data, and the old install is what
a rollback returns to. stalwart-migrator upgrades in place and says
plainly that it cannot undo one; this tool can, because it never writes to
what it migrates from.
It reads the source install and writes only to the new one. The source's store is copied, never moved, never opened for writing.
Phases, each timed
- Preflight, with nothing stopped yet: the source's version and store backend, disk for a second copy of the store, the target path empty, the ports the fork will want, the front ends' plan, and a snapshot the operator confirms exists. Anything wrong here costs no downtime.
- Stop: the mail server, then the webmail container. The unit is disabled as well as stopped, so a reboot can't put two servers on one set of ports with two diverging stores.
- Copy the store to the fork's path.
- Configure: the fork's config and unit, pointing at the copy.
- Start the fork, and wait for it to answer.
- Front ends: INBUXA Admin and the webmail, against the same store's OAuth clients.
- Verify (below). This is the gate, not a formality.
- Report.
Downtime is phases 2 to 7, and it is the number worth advertising — not the total, which preflight and the copy dominate and which nobody experiences. The report gives both, per phase, and writes them as JSON beside the human-readable summary so a release note can quote them honestly. A migration of a small server should be minutes; the number that gets published should be one this tool measured, on a stated store size and account count, not an estimate.
Verification, before mail flows again
The same checks as cutover.md, run by the tool rather than by hand:
- the fork answers on every listener the source had, with the certificate the source was serving;
- an account signs in over IMAP, JMAP and both front ends, with the credential it already had;
- a message arrives from outside and one leaves;
- aliases still receive; a tenant administrator sees its own accounts and domains and no others;
- the queue drains and the task queue gains no permanent failures;
- nothing but the fork is listening on those ports.
A failed check is a failed migration, and the tool rolls back rather than leaving a half-migrated server for the operator to reason about at 2am.
After a migration: the stock client
A migration that passes its own verification has proved the fork works for the fork. Pointing an unmodified public ihasmail at the migrated server proves something else: that a client which knows nothing about INBUXA still works against it. It runs anywhere — JMAP and OAuth over the network — and needs only an OAuth client it may use.
Each difference it turns up is either a regression against upstream's contract or a feature that now expects INBUXA's own front ends. The tool doesn't run this; it belongs in the release notes and the contract (SPEC.md §5.2). It is named here so the first migration doesn't skip it.
Rollback
A first-class path, not an appendix.
- Automatic when verification fails, unless the operator asked for the tool to stop and wait instead. A server that half-works is worse than one that is back where it started.
- On demand afterwards, for as long as the old install is still there:
rollbackstops the fork, re-enables and starts the original unit, and brings the old webmail container back. - What it costs: everything the fork accepted while it served. The two stores diverge from the moment the fork starts, so a rollback after mail has flowed leaves that mail in the fork's store. The tool says how many messages that is rather than letting the operator find out.
- What it never does: point the old server at the store the fork has
been writing. That is not a recovery path and cannot be made into one —
the old build refuses to open a store the fork has touched at all
(
cutover.md). - What it undoes first: the read-only flag on the original store. The guard that stops the fork opening it by mistake also stops the old build starting on it.
- The old install is kept until the operator retires it deliberately. The
tool does not delete the thing its rollback depends on, and when the
operator is done with it the tool asks — keep, or delete — rather
than deciding. Nothing the fork runs on may be inside it by then, which
verifychecks: no open file under the old tree, and no path in the fork's config or unit naming it.
Rehearsal
--dry-run does everything but the stop: copies the store to a scratch
path, brings the fork up against it on alternate ports, runs what it can of
the verification, reports the timings, and removes the copy. It answers "how
long will this take, and will it work" without touching the running server,
and it is what the operator should run first.
Open
- Where it lives: a command in ihasmail-oneshot, a sibling to
stalwart-migrator, or the fork's own binary (SPEC.md §8's open question about the installer's repository). - Whether it handles container installs as well as native ones, which changes "stop the unit" into "stop the stack".
- Whether it migrates a Community-edition install too. Nothing about the copy is Enterprise-specific, and a Community server has fewer objects rather than different ones, so this is likely a documentation question rather than a code one.
- What it does when the source is mid-upgrade, or its store format is older than the fork's base.