# Multiple Domains (Multi-Site) One Paskia instance on one port serves several domains from a single database. Typical uses: - `app1.company.com` and `app2.com` cannot share passkeys, but user management should stay under one roof. - A few alternative brand names should accept the _same_ passkeys. ## Shared vs. per-domain **Shared across all domains:** user accounts, organizations, roles, permissions, and OIDC clients. A user account exists once and can sign in on every domain. **Per domain:** passkeys and sessions. - A passkey is registered to one domain name (enforced by the browser): a passkey created for `company.com` works on `company.com` and its subdomains, never on an unrelated name — unless that name is configured as a [related origin](#related-origins-sharing-passkeys-across-domain-names). A user active on two domains simply holds one passkey per domain. - A session is bound to the exact host that issued it. ## Managing domains The admin panel's **Domains** section (master admins only) lists every domain with its allowed origins. There you can add, edit and delete domains; changes apply immediately without a restart and require a recent sign-in (within 5 minutes). The 🔑 and 🔗 markers in the origins column identify the auth host and related origins (below). A domain consists of: - **Domain (rp-id)** — the domain name passkeys belong to, e.g. `company.com`. ("rp-id" is the WebAuthn term; read it as "domain name".) It cannot be changed after creation, because existing passkeys are bound to it — delete and re-create the domain instead. - **Display name (rp-name)** — branding shown in sign-in dialogs and registered with passkeys. - **Allowed origins** — the sites where this domain's passkeys may sign in, plus any related origins. Deleting a domain is refused while any passkey is still registered to it, when it is the last remaining domain, or when it is the domain you are currently using. Users and organizations are never deleted with a domain — they are shared. An edit that would lock you out (your current site could no longer run passkey ceremonies for that domain) is refused as well. ## Allowed origins (sign-in sites) This list controls which sites may sign in with the domain's passkeys: - **Empty list (the default):** the domain and all its subdomains may sign in, on any scheme. This suits most deployments; the domain dialog shows the default as a `*` placeholder row. - **Once you add entries, the list becomes an allow-list** — only listed sites may sign in: - `app.company.com` — exactly this host, https only. - `*.company.com` — the base domain and all its subdomains, https only. Under `localhost`, wildcards match any scheme and any port, as a development convenience. - `http://localhost:8080` — a full origin with scheme, for non-https exceptions. - `*` — shorthand for `*.{domain}`: the whole domain over https. Entries must be inside the domain. An entry on a different domain name automatically becomes a related origin (🔗) instead — see below. ## The auth host (🔑) Marking one allowed origin as the **auth host** (row menu ⋮ → "Set as auth host") centralizes the account and admin interface on that site, e.g. `auth.company.com`: - On the auth host the web UI is served at the site root (`/` instead of `/auth/`), and all passkey operations for the domain happen there. - The domain's other sites show only a minimal profile page at `/auth/` with logout and a link to the full profile; their sign-in dialogs talk to the auth host behind the scenes. Every sign-in site still needs to be listed in (or covered by) the allowed origins. The auth host is strictly per-domain — domains never borrow each other's auth host. To consolidate several domains on one sign-in site, that site must lie under each domain's name (nested domains, e.g. domains `company.com` and `auth.company.com`) and be marked on each of them. ## Related origins: sharing passkeys across domain names Sometimes a few different domain names should accept the _same_ passkeys — for example after a rebrand, when `app2.com` should keep working with existing `company.com` passkeys. Adding `app2.com` to `company.com`'s allowed origins makes it a **related origin**: browsers then let `app2.com` use `company.com` passkeys directly — no redirects, no cross-domain cookies. (This uses the WebAuthn "Related Origin Requests" mechanism, which is why the UI also says ROR.) Rules: - At most **5 related origins per domain** — a browser limit. This is for a small family of equally trusted sites, not for hundreds of customer domains; use separate domains for those. - Exact hosts only — no wildcards — and always outside the domain's own name. - The browser verifies the setup against `https:///.well-known/webauthn`. Paskia serves that document automatically when it hosts the domain's main site; if the main site is hosted elsewhere, copy the JSON document shown in the domain dialog and publish it there. The dialog also checks the published document for you. - A related origin shares the domain's security boundary completely — do not mix trust levels within one domain. - When several domains could claim a host: a host that _is_ a configured domain name always serves its own domain; otherwise an explicit related origin listing wins over merely falling under another domain's name. Passkeys never move between domains. If you later consolidate separate domains onto one, users re-enroll: sign in once via remote authorization (below), then register a new passkey for the common domain from the profile page. ## Signing in across domains Users exist once, but need a passkey per domain. Two mechanisms smooth this over: - **Remote authorization:** a user without a passkey for the current domain can start a login request and approve it from any device already signed in — on _any_ domain of the instance. The approval screen shows which site is requesting access. - **Enroll on the spot:** when the signed-in user has no passkey for the current domain, the profile page offers "Add Passkey for {domain}", so everyday sign-in stays local from then on. ## OIDC with multiple domains OIDC clients are shared by the whole instance: register a client once and it works through every domain. Each domain serves its own discovery URL (`https:///.well-known/openid-configuration`), listed in the admin OIDC client view. Have each app pick **one** discovery URL and use it consistently, so its tokens always validate against the same issuer. ## Command line The admin panel covers all domain management after bootstrap. On the command line: - `paskia init [domain] [name]` — creates the database `paskia.kantadb` with the first domain. Run again with an existing database to add another domain (or update a display name). - `paskia migrate [domain]` — converts a legacy 1.x `{domain}.paskiadb` database to `paskia.kantadb`; see below. - `paskia` — serves all configured domains; takes no domain options, only `--listen` as a per-run override. ## Upgrading from 1.x 2.0 intentionally changes the on-disk layout and the domain configuration model: - The database is the single file **`paskia.kantadb`** in the working directory; user files (avatars) live in **`paskia.data/users/`**. `paskia migrate` performs the conversion and renames the old database aside to `{domain}.paskiadb.converted-bak`. With several legacy databases, the positional argument selects one by name. Legacy wildcard origins convert as-is (https only, except any scheme and port under `localhost`). - Origins, auth hosts and related origins are no longer environment settings — they live in the database and are managed in the admin panel's Domains section. `PASKIA_AUTH_HOST` remains only as a development-server (vite) setting. - OIDC becomes instance-global: one signing key and one client set, reachable through every domain's discovery URL (previously each rp-id had its own). Existing clients keep working through any domain.