Serve more than one Stalwart from one ihasmail, choosing the upstream by the domain someone signs in with. Nobody has asked for this yet; filing it so the scope exists when they do.
Scope
STALWART_URL stays required and stays the default. An installation that sets nothing else behaves exactly as it does now — this is purely additive.
An optional mapping file adds domains that go somewhere else:
Read once at boot, never written, mountable read-only — so it costs nothing in immutability, the same as the settings policy (confirmed against the real image on 2026-09-02).
The routing rule
The domain is taken from the username at sign-in.
Mapped domain → that server, and only that server.
Unmapped domain, or a username with no domain at all (Stalwart accepts bare usernames) → STALWART_URL.
A mapped domain must never fall back to the default. If example.com maps to a server that is unreachable, that sign-in fails — it does not retry against the default. Falling back would authenticate someone against a server their domain was deliberately routed away from, and if the same account name exists there they would land in the wrong tenant's mailbox. The fallback is a decision about unmapped domains, made before any network call, not a recovery path.
What changes
Smaller than expected. Only four places read config.stalwartUrl, all in server/src/upstream.ts:
upstream.ts:28 — .well-known/jmap discovery
upstream.ts:215 — the /api/account call
upstream.ts:263 — absoluteUpstream, which resolves relative upstream URLs
index.ts:10 — a startup log line
So the work is: resolve the upstream once at login, store it on the session record, and thread it through those functions instead of reading config. Every proxied route (/api/jmap, /api/upload/:accountId, /api/blob/…, /api/events, the account and app-password routes) already carries the session, so each gets the right upstream for free.
The client needs no changes at all. It talks to one proxy and never learns there is more than one server behind it. That is what keeps this small.
Validation
Following the settings policy's precedent — fail loudly at startup rather than quietly at sign-in:
Malformed JSON stops the server.
A value that is not an absolute http(s) URL stops the server.
Domains are lower-cased and stripped of a trailing dot on load; a duplicate after normalising stops the server.
Servers are not probed at boot. One being down must not stop ihasmail starting.
Explicitly not in scope
Several servers for one person at once. JMAP account ids are only unique per server — ours is literally n — so two Stalwarts both hand you an account called n. Supporting both at once means namespacing ids through the proxy and rewriting request and response bodies including #ids and resultOf chains. That is a rewrite of the identity model, not this issue.
Unified or cross-account views. Merged paging cannot be offset without fetching from every source, threads do not cross servers, push is per-connection, and the settings file would need a home. A different product.
Per-domain branding or settings. Separate concern; the settings policy from #207 is per-installation.
Note
JMAP sharing already covers the common case this gets confused with — other people's mail, calendars and address books on the same server, which a multi-tenant Stalwart provides natively. This issue is only about different servers, which is the case where one ihasmail fronts several unrelated Stalwart installations.
Rebuilt from: session transcript.
Serve more than one Stalwart from one ihasmail, choosing the upstream by the domain someone signs in with. Nobody has asked for this yet; filing it so the scope exists when they do.
## Scope
**`STALWART_URL` stays required and stays the default.** An installation that sets nothing else behaves exactly as it does now — this is purely additive.
An optional mapping file adds domains that go somewhere else:
```json
{
"example.com": "https://mail.example.com",
"customer-b.test": "https://jmap.customer-b.test"
}
```
Passed at startup, the same shape as the settings policy in #231:
```bash
-e STALWART_SERVERS_FILE=/etc/ihasmail/servers.json \
-v /srv/ihasmail/servers.json:/etc/ihasmail/servers.json:ro
```
Read once at boot, never written, mountable read-only — so it costs nothing in immutability, the same as the settings policy (confirmed against the real image on 2026-09-02).
## The routing rule
The domain is taken from the username at sign-in.
- **Mapped domain** → that server, and **only** that server.
- **Unmapped domain, or a username with no domain at all** (Stalwart accepts bare usernames) → `STALWART_URL`.
**A mapped domain must never fall back to the default.** If `example.com` maps to a server that is unreachable, that sign-in fails — it does not retry against the default. Falling back would authenticate someone against a server their domain was deliberately routed away from, and if the same account name exists there they would land in the wrong tenant's mailbox. The fallback is a decision about *unmapped* domains, made before any network call, not a recovery path.
## What changes
Smaller than expected. Only four places read `config.stalwartUrl`, all in `server/src/upstream.ts`:
- `upstream.ts:28` — `.well-known/jmap` discovery
- `upstream.ts:215` — the `/api/account` call
- `upstream.ts:263` — `absoluteUpstream`, which resolves relative upstream URLs
- `index.ts:10` — a startup log line
So the work is: resolve the upstream once at login, **store it on the session record**, and thread it through those functions instead of reading config. Every proxied route (`/api/jmap`, `/api/upload/:accountId`, `/api/blob/…`, `/api/events`, the account and app-password routes) already carries the session, so each gets the right upstream for free.
**The client needs no changes at all.** It talks to one proxy and never learns there is more than one server behind it. That is what keeps this small.
## Validation
Following the settings policy's precedent — fail loudly at startup rather than quietly at sign-in:
- Malformed JSON stops the server.
- A value that is not an absolute `http(s)` URL stops the server.
- Domains are lower-cased and stripped of a trailing dot on load; a duplicate after normalising stops the server.
- Servers are **not** probed at boot. One being down must not stop ihasmail starting.
## Explicitly not in scope
- **Several servers for one person at once.** JMAP account ids are only unique per server — ours is literally `n` — so two Stalwarts both hand you an account called `n`. Supporting both at once means namespacing ids through the proxy and rewriting request and response bodies including `#ids` and `resultOf` chains. That is a rewrite of the identity model, not this issue.
- **Unified or cross-account views.** Merged paging cannot be offset without fetching from every source, threads do not cross servers, push is per-connection, and the settings file would need a home. A different product.
- **Per-domain branding or settings.** Separate concern; the settings policy from #207 is per-installation.
## Note
JMAP sharing already covers the common case this gets confused with — other people's mail, calendars and address books on the same server, which a multi-tenant Stalwart provides natively. This issue is only about *different servers*, which is the case where one ihasmail fronts several unrelated Stalwart installations.
<sub>Rebuilt from: session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Serve more than one Stalwart from one ihasmail, choosing the upstream by the domain someone signs in with. Nobody has asked for this yet; filing it so the scope exists when they do.
Scope
STALWART_URLstays required and stays the default. An installation that sets nothing else behaves exactly as it does now — this is purely additive.An optional mapping file adds domains that go somewhere else:
Passed at startup, the same shape as the settings policy in #231:
Read once at boot, never written, mountable read-only — so it costs nothing in immutability, the same as the settings policy (confirmed against the real image on 2026-09-02).
The routing rule
The domain is taken from the username at sign-in.
STALWART_URL.A mapped domain must never fall back to the default. If
example.commaps to a server that is unreachable, that sign-in fails — it does not retry against the default. Falling back would authenticate someone against a server their domain was deliberately routed away from, and if the same account name exists there they would land in the wrong tenant's mailbox. The fallback is a decision about unmapped domains, made before any network call, not a recovery path.What changes
Smaller than expected. Only four places read
config.stalwartUrl, all inserver/src/upstream.ts:upstream.ts:28—.well-known/jmapdiscoveryupstream.ts:215— the/api/accountcallupstream.ts:263—absoluteUpstream, which resolves relative upstream URLsindex.ts:10— a startup log lineSo the work is: resolve the upstream once at login, store it on the session record, and thread it through those functions instead of reading config. Every proxied route (
/api/jmap,/api/upload/:accountId,/api/blob/…,/api/events, the account and app-password routes) already carries the session, so each gets the right upstream for free.The client needs no changes at all. It talks to one proxy and never learns there is more than one server behind it. That is what keeps this small.
Validation
Following the settings policy's precedent — fail loudly at startup rather than quietly at sign-in:
http(s)URL stops the server.Explicitly not in scope
n— so two Stalwarts both hand you an account calledn. Supporting both at once means namespacing ids through the proxy and rewriting request and response bodies including#idsandresultOfchains. That is a rewrite of the identity model, not this issue.Note
JMAP sharing already covers the common case this gets confused with — other people's mail, calendars and address books on the same server, which a multi-tenant Stalwart provides natively. This issue is only about different servers, which is the case where one ihasmail fronts several unrelated Stalwart installations.
Rebuilt from: session transcript.