Architecture
oidc-exchange is built with hexagonal architecture (ports and adapters). All infrastructure (databases, key management, audit systems, identity providers) sits behind trait interfaces defined in the core crate. The core business logic has zero infrastructure dependencies. Adapters are selected at runtime from configuration.
Crate structure
Section titled “Crate structure”The project is a Cargo workspace with six crates:
crates/├── core/ # Domain types, port traits, service logic (zero infra deps)├── adapters/ # DynamoDB, KMS, SQS, OIDC, webhook implementations├── providers/ # Non-standard provider modules (Apple; atproto planned)├── server/ # Axum routes, middleware, telemetry, bootstrap├── ffi/ # Language-agnostic request/response FFI wrapper└── test-utils/ # Mock implementations for all ports| Crate | Package name | Purpose |
|---|---|---|
crates/core |
oidc-exchange-core |
Domain types, port traits, and service logic. No infrastructure crates (no AWS SDKs, HTTP clients, or database drivers). |
crates/adapters |
oidc-exchange-adapters |
Implementations of port traits for DynamoDB, PostgreSQL, SQLite, Valkey, LMDB, KMS, SQS, standard OIDC, and webhooks. |
crates/providers |
oidc-exchange-providers |
Non-standard identity provider modules (Apple; atproto planned) that need custom logic beyond the generic OIDC adapter. |
crates/server |
oidc-exchange |
HTTP layer (axum), middleware, telemetry setup, configuration loading, and the binary entrypoint. |
crates/ffi |
oidc-exchange-ffi |
Language-agnostic request/response wrapper over the server’s router construction. Reused by the Node.js and Python bindings via FFI. |
crates/test-utils |
oidc-exchange-test-utils |
In-memory mock implementations of all ports. Dev-dependency only. |
Hexagonal architecture
Section titled “Hexagonal architecture”The hexagonal architecture pattern separates business logic from infrastructure by defining abstract interfaces (ports) that the core depends on. Concrete implementations (adapters) are injected at startup.
┌─────────────────────────────────┐ │ server crate │ │ (axum routes, middleware, │ │ telemetry, bootstrap) │ └──────────────┬───────────────────┘ │ ┌──────────────▼───────────────────┐ │ core crate │ │ │ │ AppService (orchestrator) │ │ Domain types (User, Session) │ │ Port traits (interfaces) │ │ │ └──┬────┬────┬────┬────┬───────────┘ │ │ │ │ │ ┌────────┘ │ │ │ └────────┐ ▼ ▼ ▼ ▼ ▼ Repository KeyManager AuditLog IdentityProvider UserSync (adapters) (adapters) (adapters) (adapters + (adapters) providers)This means:
- Business logic in
coreis testable in complete isolation using mocks - Swapping DynamoDB for PostgreSQL means changing a config value, not rewriting the service
- Adding a new storage backend means implementing a trait, not modifying core logic
- All infrastructure concerns (network, serialization, retries) are contained in adapter crates
Port traits
Section titled “Port traits”Ports are async trait interfaces defined in crates/core/src/ports/. They define the contracts that the core depends on.
| Port | Trait | Purpose | Adapters |
|---|---|---|---|
| User storage | UserRepository |
CRUD for user records | DynamoDB, PostgreSQL, SQLite |
| Session-only storage | SessionRepository |
Optional override for session operations only | Valkey, LMDB |
| Key management | KeyManager |
JWT signing and public key export | Local (Ed25519), AWS KMS (RSA, ECDSA), Noop |
| Audit logging | AuditLog |
Compliance and security event recording | Noop, Stdout/Stderr, SQS |
| Identity provider | IdentityProvider |
Code exchange, ID token validation, revocation | Standard OIDC, Apple, atproto (planned) |
| User sync | UserSync |
Notify external systems of user lifecycle events | Webhook, Noop |
| Rate limiting | RateLimiter |
Request and failed-authentication budgets | In-process fixed window, Noop |
All ports return Result<T> using a domain-specific error type. Adapters map their internal errors (AWS SDK errors, database errors, HTTP errors) into domain errors at the boundary. No adapter-specific types leak into the core.
Dependency rules
Section titled “Dependency rules”The dependency graph enforces strict layering:
coredepends on nothing infrastructure-specific: no AWS SDKs, no HTTP clients, no database drivers. Its dependencies are pure-Rust utility crates (serde, thiserror, async-trait, chrono, tracing, and similar), never infrastructure clients.adaptersandprovidersdepend oncorefor trait definitions. They implement the port traits using real infrastructure clients.serverdepends oncore,adapters, andproviders. It wires everything together at startup.test-utilsdepends only oncore. It provides in-memory mock implementations used as dev-dependencies by all other crates.
These boundaries are enforced by the Cargo workspace. If core compiles, the domain logic is free of infrastructure coupling.
server ──────► core ◄────── adapters ▲ │ providers ▲ │ test-utils (dev only)AppService
Section titled “AppService”The AppService struct in the core crate is the central orchestrator. It holds references to all ports and implements the business logic for token exchange, refresh, revocation, and user management:
pub struct AppService { user_repo: Box<dyn UserRepository>, session_repo: Box<dyn SessionRepository>, keys: Box<dyn KeyManager>, audit: Box<dyn AuditLog>, user_sync: Box<dyn UserSync>, rate_limiter: Box<dyn RateLimiter>, providers: HashMap<String, Box<dyn IdentityProvider>>, config: Config,}All ports use dynamic dispatch (Box<dyn Trait>). Since every port operation is I/O-bound (network calls, disk reads), the nanosecond overhead of dynamic dispatch is irrelevant compared to the millisecond cost of the actual operations. This enables runtime adapter selection from configuration without monomorphization complexity.
The server crate constructs AppService at startup by reading the configuration, instantiating the appropriate adapters, and injecting them. The axum router holds an Arc<AppService> in application state and passes it to request handlers.
Runtime bootstrap
Section titled “Runtime bootstrap”The server crate’s main.rs is the entrypoint: it loads configuration, initializes telemetry, and selects the transport, then delegates adapter wiring and router construction to bootstrap.rs (build_service and build_router). The full startup sequence is:
- Load configuration (TOML file + environment variable overrides)
- Initialize the telemetry subscriber based on
[telemetry]config - Detect runtime mode: if
AWS_LAMBDA_RUNTIME_APIis set, Lambda mode; otherwise, server mode - Instantiate adapters based on config in
bootstrap::build_service(user repository, session repository, key manager, audit, rate limiter, providers, user sync) - Construct
AppServicewith injected ports, also inbootstrap::build_service - Build the axum
Routerwith routes and middleware inbootstrap::build_router - Lambda mode: wrap the router with
lambda_httpand runlambda_runtime. Server mode: bind to the configured address and run with hyper.
The same binary, the same router, and the same code paths run in both modes. Only the outermost transport layer differs.