Auth & IdP overview
assay-engine v0.2.0 ships a complete identity provider in the same binary that
runs the Temporal-replacement workflow engine. One process. PG18 or SQLite. Ory + Hydra +
Keto in ~9 MB.
What's in v0.2.0
Every primitive a serious self-hosted IdP needs, packaged into one Rust crate
(assay-auth) and composed into assay-engine:
| Module | Replaces | What it does |
|---|---|---|
session | Ory Kratos (sessions) | Cookie + CSRF + 30-day TTL session manager |
password | Ory Kratos (passwords) | Argon2id PHC strings, peppered hashing |
jwt | Hydra (JWT) | RS256 issue/verify with rotated JWKS (active + history) |
oidc (client) | Kratos (federation) | Log in via Google / Apple / GitHub / any OIDC IdP |
oidc_provider | Ory Hydra | Full /authorize, /token, /userinfo, RFC 7009 revoke, RFC 7662 introspect, back-channel logout |
passkey | Kratos (WebAuthn) | webauthn-rs-backed passkey register + auth ceremonies |
zanzibar | Ory Keto / SpiceDB | ReBAC tuples + recursive-CTE walk on PG18 + SQLite |
biscuit | (Ory has nothing) | Datalog-attenuable capability tokens — always-on |
admin | Ory Console (HTTP API) | Cross-cutting users / sessions / Zanzibar / keys / audit endpoints |
Why ditch Ory for assay-engine?
- One static binary. ~9 MB stripped,
FROM scratch-shippable. Replaces a stack of Kratos + Hydra + Keto + Oathkeeper containers (typically 800 MB-1.5 GB compressed across 5+ images). - Backend symmetry. PG18 + SQLite are both first-class. SQLite means a self-hosted single-tenant deployment needs no database server at all — unique vs Ory.
- Biscuit out of the box. Datalog-attenuable capability tokens that callers can scope down further without a server round-trip. Ory has nothing equivalent; this is a real differentiator.
- Workflow + auth share storage. Atomic transactions across
auth.users↔workflow.workflows(cross-schema FKs on PG, both attachments on SQLite). Splitting Ory + Temporal forces 2-phase commit. - Lua-scriptable. Every auth surface is reachable from the
assay.authLua stdlib module — operators build login, admin, and federation flows in scripts the runtime binary ships with. - Module enablement is runtime, not build-time. Compile auth in once;
flip
engine.modules.auth.enabledper environment without redeploying.
Storage model
All auth tables live in the auth schema (PG) or attached auth
database (SQLite, default ./data/auth.db). Migrations are idempotent and
version-tracked in the shared engine.migrations table:
| Table | Purpose |
|---|---|
auth.users | Authoritative user records |
auth.sessions | Opaque session ids + CSRF tokens + expiry |
auth.passkeys | WebAuthn credentials per user |
auth.user_upstream | Federated identity links |
auth.audit | Append-only compliance log |
auth.jwks_keys | Rotated JWT signing keys (active + history) |
auth.biscuit_root_keys | Biscuit capability-token root keys |
auth.zanzibar_tuples | ReBAC relation tuples |
auth.zanzibar_namespaces | Per-namespace schema definitions |
auth.oidc_clients | Registered consumer apps (engine = OP) |
auth.upstream_providers | Federated identity providers |
auth.oidc_authorization_codes | Single-use codes from /authorize |
auth.oidc_refresh_tokens | SHA-256-hashed long-lived bearer tokens |
auth.oidc_sessions | SSO session registry (one per id_token) |
auth.oidc_consents | Per-(user, client) consent grants |
auth.oidc_upstream_states | Short-lived federation state |
Minimum viable config
# engine.toml
auto_enable_modules = ["auth"]
[server]
bind_addr = "0.0.0.0:3000"
public_url = "https://auth.example.com" # OIDC iss + passkey origin
[backend]
type = "postgres"
url = "postgres://postgres:postgres@localhost/assay"
# Or: type = "sqlite", data_dir = "./data"
[auth]
issuer = "https://auth.example.com/auth"
admin_api_keys = ["sk_admin_replace_me"]
[auth.passkey]
rp_id = "auth.example.com"
rp_name = "Acme Identity"
[auth.oidc_provider]
enabled = true
assay-engine serve --config engine.toml
# /auth/console → admin SPA
# /.well-known/openid-configuration → OIDC discovery (Hydra-equivalent)
# /auth/login, /auth/passkey/* → user-facing auth flows
# /auth/admin/auth/* → admin HTTP API (api-key gated)
Dashboard panes
When the auth module is enabled in engine.modules, the engine's dashboard SPA
lights up with these panes:
- Users — list, search, view (with passkeys + sessions + upstream links), enable/disable, delete, send password-reset.
- Sessions — global session list, per-user view, revoke single, revoke-all-for-user.
- OIDC clients — CRUD for consumer apps that authenticate against this IdP. Create + view secret-once + rotate-secret + delete.
- OIDC upstream providers — CRUD for federated SSO sources.
- Zanzibar — namespace browser, tuple inspector with filter, check evaluator, expand viewer.
- JWKS / Biscuit keys — list active + history, view public material, trigger rotation.
- Audit log — paginated viewer with filter by actor / action / time range.
Where to next?
- Passkey login flow — code samples + Lua API
- OIDC provider quick-start — register a client + plug into Immich/Grafana/etc.
- Zanzibar permissions guide — schema, tuples, check API
- Biscuit capability tokens — issue / verify / attenuate
- Compare vs Ory — side-by-side feature matrix