This directory records Authling's current runtime components and operational contracts. Keep planned architecture in ADRs until it is implemented.
The authling command exposes help, version,
and run. run loads the standalone configuration, opens Authling's NATS
storage, starts every required projection, waits for startup replay, starts the
HTTP listener, and then runs until its process context is cancelled.
The HTTP surface contains server-rendered signup, login, consent, account, and logout pages plus embedded browser assets. It also exposes OpenID Connect discovery, authorization, token, UserInfo, and JWKS endpoints and an experimental authenticated account-data WebSocket. Authling still exposes no public account-management API.
Chatto's bundled frontend is the first account-data client. It authorizes as a
dedicated CIMD public client selected by the frontend origin's trusted
/client-config.json, retains the short-lived access token in browser-local
storage across browser sessions, reads the account ID from UserInfo, and
synchronizes only public server-registration fields. Chatto server login uses a
different CIMD client. A matching advertised issuer lets the frontend start
that separate authorization with the existing Authling browser session. Chatto
server credentials remain in the browser's separate local registry.
The runtime reads authling.toml by default. AUTHLING_* environment variables
override TOML values. Unknown TOML fields fail decoding.
http.bind_address selects the public HTTP listener and defaults to
127.0.0.1:8080. AUTHLING_HTTP_BIND_ADDRESS overrides it.
http.public_url declares Authling's externally visible origin and controls
browser cookie transport policy. An http:// origin is valid only when both
the origin and listener are loopback; every other deployment must configure an
https:// origin. AUTHLING_HTTP_PUBLIC_URL provides the equivalent override.
Requests with another Host are rejected, and unsafe browser requests must
carry a matching Origin; Fetch Metadata is an additional cross-site signal.
The listener itself is plain HTTP, so production deployments terminate HTTPS
at a reverse proxy. HTTPS deployments use a host-bound __Host- session cookie;
the unprefixed cookie name exists only for loopback development.
http.trusted_proxy_cidrs identifies direct reverse-proxy networks for
account-data handshake admission. Only those peers may supply the single,
sanitized client IP in X-Forwarded-For. Authling otherwise uses the direct
TCP peer and does not trust forwarding headers.
authentication.password_minimum_length sets the local signup password
minimum and defaults to ten Unicode characters. Values from eight through 128
are accepted. AUTHLING_AUTHENTICATION_PASSWORD_MINIMUM_LENGTH provides the
equivalent environment override; the 1,024-byte maximum remains fixed.
The smtp section configures transactional email. When enabled, host,
port, and from are required. TLS defaults to mandatory STARTTLS (or
implicit TLS on port 465); opportunistic is an explicit local-development
fallback. Fields have corresponding AUTHLING_SMTP_* environment overrides.
Each [[oidc.clients]] table declares a conventional OIDC client with id,
name, and one or more exact redirect_uris. An omitted secret creates a
public client; a secret of at least 32 characters enables
client_secret_basic. URL client IDs are reserved for CIMD and need no local
configuration. HTTPS redirects are mandatory outside loopback development.
oidc.cimd_trusted_private_hosts is an explicit development-network exception
that permits named CIMD hosts to resolve to private, but no other special-use,
addresses.
Operators must select exactly one NATS mode:
nats.embedded.enabled = truestarts a private in-process NATS server with JetStream and no TCP listener. Its file-backed state lives innats.embedded.data_dir, which defaults to.authling/nats.nats.clientconnects to an external URL using a NATS credentials file. Credentials are mandatory so Authling uses its own NATS account.
JetStream resources use one replica by default. Explicit replica counts may be one, three, or five.
The application-neutral embedded server lifecycle comes from
hmans.de/chatto/pkg/natsruntime; Authling retains its private-listener,
storage-path, logging, and deployment policy.
| Resource | Kind | Storage | Subjects | Purpose |
|---|---|---|---|---|
AUTHLING_EVT |
Stream | File, S2-compressed | authling.evt.> |
Authoritative Authling event history |
AUTHLING_RUNTIME_STATE |
KV bucket | File, history 1 | Opaque HMAC-derived keys | Encrypted signup, session, OIDC request, code, and access-token state, plus bounded delivery and login-attempt counters |
AUTHLING_KEYS |
KV bucket | File, history 1 | Opaque key references | Workflow, OIDC signing, user, and wrapped credential data keys |
AUTHLING_USER_DATA |
KV bucket | File, history 1, compressed, 384 KiB record limit | HMAC-derived account keys | Encrypted TinyBase account data spaces |
AUTHLING_EVT enables JetStream atomic publication for future multi-event
commands. The key bucket is a separate, exceptionally sensitive backup and
restore boundary.
One account data space uses a random purpose-scoped data key wrapped by its account user key. The encrypted KV envelope authenticates its opaque state key, data-key reference, purpose, and version. KV revision checks provide the cross-replica write boundary; a losing writer reloads, merges its TinyBase changes, and retries.
Credential provisioning writes an opaque operation record before creating its user and data keys, then removes the marker after the referencing event commits. Normal command failures compensate immediately. Crash orphans remain discoverable by their durable marker; Authling does not use time alone as authority to delete keys that an in-flight replica could still reference.
Persisted records use the authling.core.v1.Event protobuf envelope:
| Event | Subject | Aggregate | Contents |
|---|---|---|---|
AccountCreatedEvent |
authling.evt.account.{accountId} |
Account | Opaque account ID and envelope creation time |
EmailClaimedEvent |
authling.evt.account-registry |
Account registry | Opaque account ID only |
IssuerEstablishedEvent |
authling.evt.issuer |
Issuer singleton | Immutable issuer URL and opaque signing-key reference and ID |
The account ID is restricted to one NATS-safe token. Structural account
creation uses per-account OCC. Verified local account creation atomically
publishes AccountCreatedEvent to the per-account subject and
EmailClaimedEvent to the PII-free registry subject. OCC guards both the new
account aggregate and current registry tail, serializing email claims across
replicas without a durable email-derived index.
The account model consumes authling.evt.account.* and
authling.evt.account-registry. It maps opaque account IDs to creation times.
During replay it resolves and decrypts local credentials and rebuilds a keyed
digest index of normalized emails. It retains encrypted verifier fields and
opaque key references, but neither plaintext email nor plaintext password
verifiers. Local authentication resolves and decrypts a verifier only for one
bounded Argon2id comparison; absent accounts resolve a persistent synthetic
key hierarchy and encrypted dummy verifier through the same storage path.
The runtime does not become ready until the projection has replayed its captured startup history. A decode or apply failure fails the projection and runtime. After account creation commits, the account service waits for the committed stream position before returning the projected account.
The account projection is currently cold-replay-only. It has no snapshot or local-checkpoint persistence.
The issuer projection consumes the singleton authling.evt.issuer subject.
On first initialization, its service creates or resolves the RS256 signing key
and establishes the issuer with subject-level OCC. Every later startup requires
the configured public URL and stored signing-key identity to match that event.
Issuer or key drift prevents readiness.
The HTTP handler renders HTML with templ. Vite compiles Tailwind CSS, IBM Plex
Sans, and Iconify glyphs during the build; the resulting assets are embedded
in the Go executable and served below /assets/. The runtime has no Node.js or
third-party asset-host dependency.
The initial Content Security Policy prohibits scripts and third-party content. All essential future authentication interactions must continue to work through ordinary server-rendered links and forms.
GET /signup renders the email form. Three POST endpoints start a flow, verify
its code, and complete account creation with a password. Unsafe requests reject
cross-origin browser submissions. The browser carries a random opaque flow
token in hidden fields; raw email addresses, OTPs, and passwords never enter
URLs.
GET /login renders local credential login. POST /login applies a shared,
keyed attempt limit before checking the encrypted credential and creates a
fresh browser session on success. GET /account requires that session, and
same-origin POST /logout revokes it. Successful signup also starts a session.
The host-only browser cookie carries only a random opaque bearer and is
HttpOnly, SameSite=Lax, scoped to /, non-persistent, and secure outside
the explicit loopback development mode.
Session records are authenticated-encrypted in runtime state beneath HMAC-derived keys. They have a 24-hour absolute lifetime and a one-hour inactivity limit. Activity updates use OCC and never extend the absolute deadline. Logout deletes the server record before clearing the cookie.
OpenID Connect mounts discovery at /.well-known/openid-configuration and its
protocol endpoints below /oauth/. Authorization accepts only code flow,
requires openid and S256 PKCE, and optionally accepts account_data.
Signed-out requests resume through an opaque server-side request ID after
login; GET and same-origin POST /oidc/consent display and record
per-request consent.
Conventional clients resolve from configuration. Unconfigured HTTPS URL client
IDs resolve through the bounded CIMD fetcher, which disables redirects and
proxies, validates DNS destinations before fetch and dial, and caps fetch time,
body size, concurrency, and cache lifetime. Pending requests, code mappings,
and opaque access-token records are encrypted and expire in runtime state.
Authorization-code claim uses KV OCC so concurrent exchange has at most one
winner. ID tokens use the persistent RS256 key; JWKS publishes only its public
part. The initial UserInfo response contains only the account ID as sub.
GET /data/sync upgrades an exact-origin request with a valid browser session
to the experimental TinyBase 9.3 synchronization transport. A client with an
account_data access token can instead use the
authling.account-data.v1 subprotocol from the exact callback origin. It must
send the token in a bounded first message and receive ready before TinyBase
messages begin. The validated session or token alone selects one account-owned
data space. The endpoint revalidates authorization for incoming and outgoing
messages, limits authentication time, pending unauthenticated connections,
frame and state size, live connections, and pending protocol requests, and
rejects invalid or future-clock input before persistence. Process-local hubs
provide live fanout. They cap all live connections and retained decrypted
spaces, evict idle spaces under pressure, and load different accounts without
holding one global lock across storage or cryptographic work. Durable KV OCC,
not the hub, protects concurrent Authling replicas.
The HTTP server bounds header, body-read, response-write, and idle time. Signup also caps request bodies, globally limits OTP delivery, and bounds concurrent SMTP calls per process.
The runtime does not yet contain recovery, account erasure, session lists or account-wide session revocation, OIDC refresh tokens or key rotation, application-scoped document namespaces, diagnostic endpoints, or backup tooling.
The runtime does not yet contain application-scoped data grants, a general document CRUD API, or cross-replica live fanout. The original TinyBase durable peer proof remains as a pinned transport compatibility test.