Architecture
The service uses a ports-and-adapters structure. Application services depend on interfaces; bootstrap supplies PostgreSQL, cryptography, passkey verification, and delivery implementations. A single Bun process serves the browser UI and APIs and runs background delivery.
Source map
| Path | Responsibility |
|---|---|
src/domain/identity.ts | Domain types, identifiers, validation, and public profiles |
src/application/ports.ts | Store, clock, crypto, mail, and passkey interfaces |
src/application/auth_service.ts | Registration, sessions, profile, MFA, passkeys, and recovery |
src/application/management_service.ts | Company ownership and platform administration |
src/application/oidc_service.ts | Discovery, client registration, authorization, and tokens |
src/application/delivery_service.ts | Email and signed back-channel logout jobs |
src/adapters/inbound | Routing, HTTP validation, cookies, assets, and security headers |
src/adapters/inbound/documentation.ts | Public documentation pages rendered from the repository Markdown guides |
src/adapters/outbound | PostgreSQL, crypto, WebAuthn, mail, logout transport, and tracing |
src/bootstrap | Configuration, dependency wiring, worker scheduling, and shutdown |
public/app.ts | Browser account and administration interface |
public/docs.js | Accessible code-copy interaction for public documentation |
public/awwwth_sdk.ts | Popup authorization helper served as /awwwth.js |
scripts | Key setup, migrations, and operator administrator grant |
migrations | Ordered SQL schema changes |
Requests pass through WebAdapter (host validation, assets, security headers), then HttpAdapter (routing, request validation, cookies, rate limits), then application services and their outbound adapters.
Merchant identity layer
Merchant dashboards and stores use the Awwwth SDK/API. Awwwth is the Identity Service: it registers merchant applications, maintains local identities and company permissions, and issues app-facing OIDC tokens. With AUTH_BACKEND=keycloak, credentials, email verification, MFA and passkeys belong to Keycloak behind that layer. The merchant-facing issuer stays Awwwth.
Merchant dashboard / store → SDK / API → Awwwth Identity Service → Keycloak
↘ Checkout Service
↘ Risk Engine
Checkout and Risk are separate consumers of Awwwth authentication; this repository does not implement cart, payment, order or fraud services. Those services must validate Awwwth tokens and enforce their own merchant, order and risk permissions. In particular, a valid identity token is not payment authorization.
Branded Keycloak forms and account settings use Awwwth's public origin. KeycloakProxy forwards only realm routes and resources to a separate private backend after Host validation. Provider CSP and flow cookies are preserved; Awwwth session cookies and untrusted forwarding headers are removed. The provider's /admin and management endpoints stay outside that public path. See the Keycloak guide for setup, migration and session behavior.
Identity and company boundaries
With the native backend, a platform user (tenant_id = NULL) owns shared credentials: password, passkeys, MFA and recovery. Company profiles (tenant_id set) retain separate IDs, sessions, application grants and the user role. New company profiles use identity_user_id to refer to the shared identity and store no password hash. New signup creates the shared identity and company profile atomically, with email verification still required before OAuth access. Existing independent company accounts remain independent until the user proves fresh ownership of both verified, same-email accounts through POST /api/account/link; matching emails alone do not link them.
Shared sign-in resolves the credential owner, binds MFA/passkey challenges to the requested company, verifies credentials, then atomically creates or checks the company profile and issues its scoped session. Public company signup is already permitted; creating a profile grants no ERP or storefront application permissions. Disabled company profiles reject login. Disabling the shared identity invalidates linked sessions and grants at live checks. Shared password resets and global session revocation revoke linked company grants, too. Company-only revocation remains scoped. Shared security settings are managed on the main Awwwth account; company pages link there. Company-session reauthentication verifies the shared password/MFA or passkey against the credential owner, and updates only the requesting company session. Reauthentication challenges are bound to that exact session and are one-use. Existing company profile IDs and roles remain unchanged when explicitly linked.
An active, verified platform user with fresh authentication can create a company. Company management uses the owner's platform session and checks ownership, including for users with the administrator role. Company members do not gain management access. The browser selects a company through ?tenant=<company-id> and sends X-Awwwth-Tenant on API requests. Sessions use separate cookie names per company. OAuth clients belong to either the platform or one company; authorization checks that the user and client directories match.
Authentication lifecycle
The following credential lifecycle describes the native backend; Keycloak mode delegates those operations to the provider while retaining local account and app authorization.
Registration creates a user, session, verification challenge, and queued verification email in a transaction. Password login returns either a session or a short-lived MFA challenge. Passkey login requires user verification and validates the credential against the configured issuer origin and relying-party hostname.
Sessions last 30 days unless revoked. Sensitive changes require authentication within the last five minutes. Password or passkey reauthentication refreshes that timestamp. Password changes and resets revoke sessions; application grants are checked against session and account status. Password policy currently accepts 6–128 characters.
Passwords use Argon2id. Random session, challenge, and OAuth tokens are stored as SHA-256 hashes. TOTP secrets and queued email bodies use AES-256-GCM. ID tokens use Ed25519 signatures.
Data model
| Tables | Stored state |
|---|---|
users, companies, addresses | Accounts, company ownership, and saved addresses |
external_identities, external_auth_attempts, external_sessions, external_logout_events | Keycloak subject mappings, one-use browser bindings, encrypted upstream tokens, and logout replay protection |
sessions | Hashed session tokens, expiry, freshness, and revocation |
challenges | Verification, recovery, MFA, and passkey challenges |
passkeys, recovery_codes | Credential public keys and hashed recovery codes |
clients, grants | Registered applications and session-bound authorization grants |
authorization_codes, tokens | One-time codes and access/refresh token state |
audit_log | Account activity records |
outbox | Pending email and logout work |
rate_limits | Shared request counters and expiry |
schema_migrations | Migration filenames, checksums, and application timestamps |
The migration runner uses an advisory lock and a transaction per migration. It verifies SHA-256 checksums for previously applied files. Add new migrations instead of editing applied ones. Migration 003_company_auth.sql explicitly requires draining older application processes before applying the tenant boundary change.
Background work and shutdown
The worker runs every second without overlapping its own executions. It claims up to 20 outbox jobs with FOR UPDATE SKIP LOCKED and a 60-second lease. Failures use exponential retry delay capped at one hour. At 24 attempts, a failed job is logged and removed; there is no separate dead-letter queue. Delivery consumers should tolerate retries.
Expired rate limits are pruned approximately once a minute. SIGINT/SIGTERM stops the worker and HTTP server, waits for active work, closes PostgreSQL, and shuts down tracing.
Development checks
Run bun run build after changes and bun run test for CLI/security tests. bun run test:sso exercises two-app SSO against a disposable PostgreSQL cluster; see the CLI guide for prerequisites. There is no lint script. For behavior changes, verify the affected flow against a development database: registration and verification, login and logout, tenant separation, reauthentication, MFA/passkeys, or OAuth code exchange as appropriate.
Shared identity rollout (2026-09-29)
Deployed the focused shared-identity patch to the existing Awwwth Docker service with migration 010_shared_identity.sql. The migration is additive and leaves existing users/passkeys untouched. The running image was used as the deployment baseline; unrelated local browser/pagination edits were excluded. The previous image is retained as awwwth-auth:before-shared-identity-20260929; the old image can be restarted without dropping the additive column, but shared company sign-in requires the new code once linked profiles exist. Prefer a fix forward after enrollment. Do not remove the new column or linked profiles.
Verification: local production build and the isolated deployment build (Bun 1.4.0) passed; ten disposable PostgreSQL/cryptographic tests and eight module tests passed. Shared password/passkey/MFA login, scoped OIDC subjects and claims, one-use challenges, company switching rejection, explicit legacy linking, disabled-root introspection and password-reset revocation were checked. Live health and deployed UI assets passed; actual account passkey/device verification was not automated, and no production users or grants were created by tests.
Embedded passkey panel (2026-09-29)
The inline node module now renders a visible provider-origin passkey iframe after confidential app initiation. The passkey remains scoped to Awwwth; it is not copied to localhost. An encrypted five-minute ticket binds email, registered app, exact callback origin, company and OIDC state/nonce/PKCE. The normal account pages still forbid framing. Only /passkey/embed permits its ticket's registered origin. Native verification accepts cross-origin assertions only when the challenge was issued for that exact ticket and signed top origin; hosted login/registration/reauthentication keep their original same-origin rules. The frame uses no cookies and returns only a code callback through origin/source-checked messaging. The app's usual callback enforces browser binding and local authorization. An explicit hosted action is retained for browsers lacking embedded WebAuthn support. Direct related-origin requests do not cover plain localhost because it has no registrable origin label.
Verification: eleven disposable PostgreSQL/cryptographic tests passed, including wrong frame origin, mismatched tickets, replay rejection, confidential client authentication and PKCE. A Chromium virtual authenticator generated a real signed cross-origin assertion inside the visible panel and completed back on localhost; 390/768/1440px panel rendering, keyboard/cancel and no page errors passed. These tests did not use the user's saved hardware passkey or alter production users.