How to build, test, and change the d-OAuth authorization server. The rules here are enforced by CI, so a change that skips them will not merge.
This guide is for anyone editing the Rust source. It covers prerequisites, the quality gates, module conventions, the test-driven workflow, commit style, and the documentation contract. Operators looking to run the server should start with the README and OPERATIONS.md.
rust-toolchain.toml (stable, with rustfmt
and clippy). rustup installs it automatically on first build.cargo-nextest for the test suite:
cargo install cargo-nextest --locked.testcontainers.sqlx-cli only when you add or change a SQL query or a migration:
cargo install sqlx-cli --locked --features runtime-tokio,tls-rustls-ring.The crate uses edition 2024 and the rustfmt settings in rustfmt.toml
(2-space indent, StdExternalCrate import grouping).
cargo build --locked # debug
cargo build --locked --release # release (thin LTO, stripped)
The build is offline-safe for SQL: with SQLX_OFFLINE=true the query! macros
compile against the committed .sqlx/ cache, so no database is needed at build
time. The Docker build relies on exactly this.
Run all of these before opening a change. CI runs the same set.
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo nextest run --all-targets
cargo test --doc
cargo audit
cargo deny check
cargo nextest run is the authoritative test entry point. It runs unit,
integration (testcontainers), and end-to-end tests. cargo test --doc runs the
doc examples separately because nextest does not cover doctests.
While iterating on one area, filter to just that area:
cargo nextest run -E 'test(refresh_rotation)'
cargo nextest run -E 'test(admin_bootstrap)'
A delegated or subagent change runs only the narrowest filter that covers the touched code. The full suite runs once, at integration time.
Integration tests start their own Postgres and Redis containers and apply the embedded migrations. They need a working Docker daemon, no more. Migrations are never run by the test process against a shared database.
The crate is one library (src/lib.rs) plus a thin binary (src/main.rs).
Every module has a single responsibility:
| Area | Path | Owns |
|---|---|---|
| Configuration | src/config.rs, src/config/ |
Layered env/file load and fail-fast validation. |
| Keys | src/keys.rs |
Versioned KeyRing for the token pepper and TOTP encryption key. |
| Crypto primitives | src/crypto/ |
Constant-time compare, PKCE, token mint/hash, TOTP, secret hashing. |
| JOSE | src/jose/ |
DPoP, JAR, private_key_jwt verification, JWK parsing. |
| Postgres repositories | src/db/ |
The six domain tables. |
| Redis store | src/store/ |
Ephemeral state: codes, tokens, grants, sessions, PAR, device, DCR, rate limits. |
| Protocol handlers | src/protocol/ |
OAuth endpoints and grants. |
| Server-rendered UI | src/ui/ |
Login, consent, and device pages. |
| Middleware | src/middleware/ |
Request id, tracing/redaction, rate limit, security headers, audit. |
| Routing and serving | src/routes.rs, src/serve.rs, src/tls.rs |
Router assembly and the two listeners. |
| Operator CLI | src/cli.rs, src/cli/ |
Every administrative command. |
Rules that apply everywhere:
#![forbid(unsafe_code)] is on the crate.unwrap, expect, or panic in request paths. They are denied by the
clippy lints in Cargo.toml.thiserror; map dependency failures to the correct
client-safe error rather than stringifying them.secrecy and zeroize. A type that holds a secret must not have
a Debug impl that prints it.subtle) for any secret or token comparison.main.rs free of business logic.Every behavior change follows red, green, refactor:
Prefer a real object or an in-memory fake over a mock. Use testcontainers for
Postgres and Redis. Assert the observable contract, not an implementation dump.
One commit per logical change, in conventional-commit form:
feat(token): add token-exchange grant
fix(store): fail closed when the durability barrier is missed
docs(agents): document the module map
test(resilience): cover Redis restart recovery
The subject is imperative and lowercase after the type. The body explains why, and links the evidence file when the task process requires one. Never commit a secret, a real token, or key material.
The docs are part of the code, not an afterthought. Any behavior change updates the relevant guide in the same commit:
docs/OAUTH.md.docs/SECURITY.md.docs/ARCHITECTURE.md.docs/CONFIGURATION.md.docs/DATABASE.md.docs/REDIS.md.docs/DEPLOYMENT.md.docs/OPERATIONS.md.Then run the link checker, which CI also runs:
python3 scripts/check_doc_links.py
A command in a doc must match the real CLI. If you change a flag, change the doc in the same commit.