v0.2.0 assay-engine becomes a full Ory replacement: passkey, OIDC provider, biscuit, Zanzibar — all in one ~19 MB static binary. →

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 ~19 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:

ModuleReplacesWhat it does
sessionOry Kratos (sessions)Cookie + CSRF + 30-day TTL session manager
passwordOry Kratos (passwords)Argon2id PHC strings, peppered hashing
recoveryOry Kratos (recovery)SMTP reset links with hashed, expiring, single-use tokens
jwtHydra (JWT)RS256 issue/verify with rotated JWKS (active + history)
oidc (client)Kratos (federation)Log in via Google / Apple / GitHub / any OIDC IdP
oidc_providerOry HydraFull /authorize, /token, /userinfo, RFC 7009 revoke, RFC 7662 introspect, back-channel logout
passkeyKratos (WebAuthn)webauthn-rs-backed passkey register + auth ceremonies
zanzibarOry Keto / SpiceDBReBAC tuples + recursive-CTE walk on PG18 + SQLite
biscuit(Ory has nothing)Datalog-attenuable capability tokens — always-on
adminOry Console (HTTP API)Cross-cutting users / sessions / Zanzibar / keys / audit endpoints

Why ditch Ory for assay-engine?

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:

TablePurpose
auth.usersAuthoritative user records
auth.sessionsOpaque session ids + CSRF tokens + expiry
auth.passkeysWebAuthn credentials per user
auth.user_upstreamFederated identity links
auth.auditAppend-only compliance log
auth.jwks_keysRotated JWT signing keys (active + history)
auth.biscuit_root_keysBiscuit capability-token root keys
auth.zanzibar_tuplesReBAC relation tuples
auth.zanzibar_namespacesPer-namespace schema definitions
auth.oidc_clientsRegistered consumer apps (engine = OP)
auth.upstream_providersFederated identity providers
auth.oidc_authorization_codesSingle-use codes from /authorize
auth.oidc_refresh_tokensSHA-256-hashed long-lived bearer tokens
auth.oidc_sessionsSSO session registry (one per id_token)
auth.oidc_consentsPer-(user, client) consent grants
auth.oidc_upstream_statesShort-lived federation state
auth.password_recovery_tokensSHA-256 token digests and expiry; raw reset tokens are never stored

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.recovery]
enabled = true

[auth.recovery.smtp]
host = "${SMTP_HOST}"
username = "${SMTP_USERNAME}"
password = "${SMTP_PASSWORD}"
from = "Acme Identity "

[auth.oidc_provider]
enabled = true

# The vault module is on by default and holds every secret under one
# master key. Without unseal material the engine refuses to start rather
# than write that key into the same database as the secrets it protects.
# ASSAY_VAULT_SEAL_KEY is the default source, so no section is needed
# here. To read the key from a file instead — which keeps it out of
# /proc/<pid>/environ and out of core dumps — uncomment:
#
# [vault.sealing]
# source = "file"
# path = "/run/secrets/vault-seal-key"   # must be mode 0600
# Any string of at least 32 characters. Not recoverable — back it up.
export ASSAY_VAULT_SEAL_KEY="$(openssl rand -base64 32)"
assay-engine serve --config engine.toml
#   /auth/console                          → admin SPA
#   /.well-known/openid-configuration      → OIDC discovery (Hydra-equivalent)
#   /auth/login, /auth/recovery             → password auth and recovery pages
#   /auth/passkey/*                         → passkey flows
#   /auth/admin/auth/*                     → admin HTTP API (api-key gated)

The browser login page always offers local email/password authentication and links to an optional SMTP-backed recovery flow. Enabled OIDC upstream providers appear beneath it as optional alternatives, so a fresh installation does not need an external identity provider before its first user can sign in.

Dashboard panes

When the auth module is enabled in engine.modules, the engine's dashboard SPA lights up with these panes:

Where to next?