skip to content
docsspec
github

ADR-0008: Single-origin deployment profiles

Status: Accepted · recorded retrospectively (decision from 2026-06)

Status — ahead of the code, and its plain-http profiles cannot sign in. There is no first-run wizard and no profile switch: the appliance serves http://zaentrum.localhost on the machine it runs on, and a cluster install takes its one host from spec.hostname. And the premise that a phone or TV can sign in at a plain http://SERVER-IP does not hold: Keycloak marks its login cookies Secure, which browsers keep over plain http for localhost names only, and the Android phone and TV apps refuse plain http altogether. As written, profiles B1, B2 and C cannot sign in from another device; today only an https setup, the shape of profile D, does.

A real self-hosted install is reached from a phone and a TV on the LAN, not just a browser on the machine running the containers. Those devices cannot edit a hosts file and cannot resolve *.localhost. Any scheme that starts with "first, set up DNS" has already lost most self-hosters.

There is a second, sharper trap. Zaentrum authenticates through OIDC, and OIDC validation is exact-string matching on the issuer URL. If the server minted tokens as http://localhost/auth but the phone reached it at http://192.168.1.50, every login fails — the platform is up, the catalog is served, and nobody can sign in. A mismatched issuer host is the canonical self-host boot failure. It happens whenever ingress routing and identity configuration are answered as two separate questions, because two separate questions can disagree.

The appliance ships deployment profiles, chosen in the first-run wizard. A profile is one switch that sets the ingress routing and the OIDC issuer/redirect URIs together. They are never configured independently, so they cannot drift apart.

ProfileHow devices reach itDNS neededUse when
A — This machine only*.localhost → 127.0.0.1 (browsers resolve this themselves)nonequick single-box trial
B1 — LAN, single origin (recommended default)one host = the server's LAN IP; route by path (/, /api, /auth, /manage); issuer = http://SERVER-IP/authnonemost self-hosters; any phone or TV via http://192.168.1.50
B2 — LAN, magic wildcardzaentrum.192-168-1-50.nip.io / sslip.io — public wildcard DNS resolves to the LAN IPnone (needs internet)when subdomain routing must stay
C — mDNS / Bonjourzaentrum.local broadcast on the LAN (mDNS has no wildcard subdomains, so aliases are flat)none.local single-origin on Mac/iOS/modern Windows/Linux
D — Real DNS / TLSown domain, wildcard record + Let's Encrypt, or a local resolver with wildcard supportyespermanent setups, external exposure

B1 — single-origin path routing — is the default, for reasons that follow directly from the constraints above:

  • Phones and TVs can type an IP address. They cannot edit hosts files, and their platform resolvers ignore *.localhost. One origin at the LAN IP works from every device with zero setup.
  • No DNS anywhere — not on the network, not on the device, not on the internet.
  • One origin means one issuer host by construction. The wizard's single answer ("what address will devices use?") derives both the routes and the issuer, so the canonical boot failure cannot occur.
flowchart LR
  dev["Phone / TV / browser<br/>on the LAN"]
  dev -->|"no DNS involved"| origin["http://192.168.1.50<br/>(one origin: the server's LAN IP)"]
  subgraph appliance["Zaentrum appliance"]
    ingress["Ingress — route by path"]
    web["/ → web client"]
    api["/api → platform API"]
    auth["/auth → identity provider (Keycloak)<br/>issuer = http://192.168.1.50/auth"]
    manage["/manage → admin console"]
    ingress --> web
    ingress --> api
    ingress --> auth
    ingress --> manage
  end
  origin --> ingress

B2, C, and D are offered as upgrades in the same wizard, and switching profiles reruns the same joint update — routing and issuer move together, always.

Clients stay out of the naming question entirely: the neutral Add-Server flow accepts whatever the user types (an IP, zaentrum.local, a domain). No client assumes a particular scheme, so every profile works with every client unchanged.

  • One wizard question eliminates the most common self-host failure. There is no separate "identity URL" field to get wrong.
  • Every service must be servable under a path prefix. No absolute-root assumptions in the web clients, no hardcoded subdomain fan-out. This is a standing constraint on all public services, checked at integration time.
  • Switching profiles changes the issuer URL, which invalidates existing sessions. Acceptable: profile changes are rare, deliberate events, and re-login is the honest cost of a new address.
  • B1 is plain HTTP on the LAN. Browser features gated on secure contexts are limited, and traffic is unencrypted on the local network. Users who need TLS take profile D; the wizard says so rather than pretending http://IP is more than it is.
  • mDNS (profile C) cannot do wildcard subdomains, so profile C reuses the single-origin path layout with flat .local aliases — another reason path routing is the shared baseline rather than a special case.
  • B2 depends on a third-party wildcard DNS service and on internet reachability; it exists for setups that genuinely need subdomains, not as a default.
  • Requiring a DNS server, a hosts-file edit, or a purchased domain to boot the platform.
  • Subdomain routing as the baseline layout. Subdomains are an opt-in (B2/D), never a prerequisite.
  • Configuring the OIDC issuer independently of ingress — the two are one setting, by design.
  • Client-side assumptions about server naming. Clients take an address; the server owns its own layout.