soul.demarkus.io:6309/plans/demarkus-as-a-service.mdsoul.demarkus.io:6309/plans/remote-token-issuance.md complete reader metaPlan: Remote token issuance (invite flow)
Status: COMPLETE (recorded 2026-07-16, closed 2026-07-17). Steps 1 (join URLs, PR #261) and 2 (broker single-host mode, PR #262 + #263) shipped. Invites dropped as redundant with the broker. The one remaining item, the no-IdP capability front door, is declined: browser editing needs the broker, the broker needs OIDC, and any OIDC provider works including a self-hosted one (Dex/Keycloak/Authentik), so the ecosystem already covers the no-commercial-IdP case. Earlier design is retained below for the record.
One-line
Onboard a new person or device to a remote soul or standalone world without SSH and without hand-carrying a long-lived raw token, with zero core-server change, at every deployment tier.
Outcome: the tier map (all cases covered)
| Case | How | Status |
|---|---|---|
| Read, any tier | quic mode (library or client), no IdP | shipped |
| Write via CLI / agent / Obsidian, any tier | direct-QUIC + token or join URL, no IdP | shipped |
| Onboard people to a shared knowledge system | broker + OIDC; /knowledge-join + IdP login |
shipped |
| Browser editing (library cataloging desk) | broker mode + OIDC (any provider) | shipped (library v0.22.0); needs an OIDC app |
The only thing that requires an IdP is browser editing, and "an IdP" means any OIDC provider, self-hosted included (Dex is a single small container). That satisfies demarkus's no-central-authority principle without a bespoke auth surface. So there is no uncovered case worth building for.
Decision (2026-07-17): capability front door declined
The last open item was a no-IdP capability front door: the broker accepting a demarkus capability token as a second auth surface alongside OIDC, for someone who wants multiple browser-editing writers but refuses any external IdP. Declined:
- The library editor (cataloging desk) writes only through broker mode; the quic adapter's Publish/Append are deliberately read-only. So browser editing is inherently a broker feature.
- The broker requires OIDC, but OIDC does not mean Google/GitHub: a self-hosted provider (Dex, Keycloak, Authentik) satisfies it. Dex is tiny and single-purpose.
- Building a capability front door would reinvent a worse, less-audited version of what Dex provides for free, and would grow the broker a second auth surface to maintain.
- The people who want browser editing are collaborators/teams, who can run an IdP. A solo user editing their own soul already has the CLI (
demarkus edit), TUI, agent, and Obsidian; the browser editor is not load-bearing for the solo case.
If a concrete deployment ever needs multi-writer browser editing with no IdP at all (not even self-hosted), revisit using the "Shared shape" and "Options A/B" design below. Until then, do not build it.
Decision (2026-07-17): invites are redundant with the broker
Question raised by Fritz: with the broker running in single-host mode, do we still need invites? No.
The broker is OIDC-fronted by construction (cannot start without an issuer, client id/secret, signing key). So "running the broker" already means "having OIDC onboarding," and OIDC beats an invite code on every axis:
| invite code | broker OIDC | |
|---|---|---|
| onboard a person | paste a redeemable code | /knowledge-join <url> + existing login |
| identity | anonymous bearer | real account |
| revoke | delete the token | disable the account, or edit the world's allow list |
| secret in transit | yes (the code) | no |
| add a collaborator | mint + send | IdP invite + allowlist entry |
Invites had a real home only in the brokerless direct-QUIC tier, where step 1's join URL already covers ~80%; the remaining gap (the paste carries a long-lived raw token) is a narrow brokerless polish, so "invites in the broker minting core" was the wrong home. Dropped.
Decision (2026-07-17): the sidecar is the broker in single-host mode
The earlier revision planned a new sidecar binary in tools/. The parity goal inverted that: teach the existing broker a file-writer backend and package it for a VPS, and the "sidecar" is just the broker running beside the server. Shipped as PR #262 (SecretStore interface + file backend) and PR #263 (install.sh --with-broker --with-library, hardened systemd units, docs). Zero core-server change (the server binary is byte-identical, reads tokens.toml, hot-reloads, never writes credentials); read-only chroot posture intact (omit the broker).
Decision (2026-07-17): cert pinning deferred
Step 1 was first built with TOFU cert-fingerprint pinning (fp= in the join URL, per-host verification, pin storage). Cut before merge; the branch shipped token-only. Reasoning: a third trust state (CA / pinned / insecure) with rotation-to-re-invite coupling, migration edge cases, mixed-version sharp edges, hand-rolled VerifyPeerCertificate; HPKP precedent (browsers removed pinning in 2018 over operators bricking their own users); narrow constituency (domain hosts get Let's Encrypt). The cut is clean: fp= was an optional fragment key and token-only Parse rejects unknown keys. Self-signed servers keep --insecure; the dev cert is ephemeral so was never pinnable; Let's Encrypt leaves rotate ~60 days and must never be pinned.
Live integration test (2026-07-17)
Stood up the full single-host stack (world server + broker file mode + library) and drove it end to end. The OIDC gate was exercised with a broker-signed bearer minted from the config's signing key (the compositeVerifier accepts broker-signed id_tokens), standing in for a completed login. Verified: broker-signed bearer authenticates the MCP gateway; mark_fetch returns world docs; mark_publish through the gateway provisions a write token, appends [tokens.broker-write-soul] to the local tokens.toml live, the server hot-reloads and honors it, the doc lands; the library (quic mode) renders both the seeded doc and the broker-written one. The desk's write path (mark_publish through the broker) is proven on the file backend. Not driven live: the browser OAuth login front-end (needs a real IdP + browser) and the library in broker mode.
Shared shape: one-shot invites (deprecated, retained for the capability-front-door case)
An invite is a short-lived, single-use code that redeems for a freshly minted scoped capability token: owner creates with label/paths/ops/TTL; user redeems once via /soul-join; the raw token is generated at redemption and travels once; expired/spent codes fail closed; attenuation means an invite grants only a subset of the inviting credential's scope and never invite itself. Only relevant if the capability front door is ever built; for any broker deployment OIDC replaces it.
Options A and B (superseded, for the record)
- A: invite minted on the host, redeem in the server. Rejected: core-server surface, and the owner still SSHes per invite.
- B: invite minted over the protocol. Rejected by the build-around constraint. Its delegation model would carry into a capability front door unchanged if ever built.
Precedent notes (for the record)
Verify-only servers (nginx, sshd, htpasswd) never issue; servers owning multi-user state issue in-band (Postgres GRANT; Matrix registration tokens; Tailscale pre-auth keys). The broker sits in the second family already, via OIDC.
Sequencing (final)
- DONE: join URLs, token-only (PR #261, commit 4c5f90d).
- DONE: broker single-host mode (PR #262 + #263).
Invites in the broker minting coreDROPPED (redundant with broker OIDC).Membership (list/revoke,DROPPED (OIDC +/soul-invite, etc.)allowlist cover it).Capability front door (no-IdP multi-user)DECLINED (self-hosted OIDC via Dex covers the no-commercial-IdP case; a bespoke auth surface would be a worse Dex).
Plan complete. User-facing tier documentation added to docs/site/deployment/single-host.md (auth tiers + self-hosted OIDC note).
Broker k8s audit (2026-07-17)
Read-only audit of tools/demarkus-broker for single-host mode. Verdict: cheap. All k8s API usage funnels through mutateSecret (authz.go:190-256) and touches only Secrets and a leader-election Lease. One SecretStore interface with k8s + file implementations covered it (~5 files + main.go). Config-only: world dial addresses (InternalAddress overrides the cluster-DNS default); namespace/Secret fields relaxed in file mode. Already portable: OIDC (env-var creds), MCP TLS, probes, in-memory device/auth stores. Surprises: no SIGHUP exists in the broker (k8s reload is Secret-mount projection; the file backend self-heals via the server's own tokens-file watcher); write tokens are never revoked in code (only hash-drift reconciliation); Provision fires on the read path. All landed correctly in PR #262.
Related documents
- Soul join: join URL redemption path this plan shipped in step 1
- Five minute appliance: single-host install and broker packaging tier