Architecture Overview
Most full-stack frameworks ship as one package that pulls in everything. Rainier ships as twenty-four crates, and an application depends on exactly the ones it uses. An app that wants the queue but not HTTP pays for neither the router nor hyper.
That is not tidiness for its own sake. In Rust, a dependency you do not use is still a dependency you compile, and a crate that reaches sideways into another is a crate you cannot test in isolation. Splitting them forced every seam to be a real one.
The crates
Every crate depends on rainier-support; those edges are left out of the graph
because they say nothing. Everything else is drawn.
graph TD
support[rainier-support<br/><i>Error, futures, type-maps, inflection</i>]
ormmacros["rainier-orm-macros<br/><i>#[derive(Entity)]</i>"]
orm[rainier-orm<br/><i>the DBAL: entities, queries, DDL</i>]
container[rainier-container<br/><i>IoC, providers, facades</i>]
config[rainier-config<br/><i>config repository, .env</i>]
events[rainier-events<br/><i>the hook bus</i>]
http[rainier-http<br/><i>requests, responses, uploads</i>]
view[rainier-view<br/><i>the template engine</i>]
crypt[rainier-crypt<br/><i>ciphers, signing, keys, hashing</i>]
drivers[rainier-drivers<br/><i>Redis, Memcached, Kafka, AWS, SQL executors</i>]
middleware[rainier-middleware<br/><i>the pipeline</i>]
routing[rainier-routing<br/><i>routes, groups, URLs</i>]
validation[rainier-validation<br/><i>rules, contracts</i>]
database[rainier-database<br/><i>models, repositories, migrations</i>]
cache[rainier-cache<br/><i>the cache port, drivers, locks</i>]
scheduler[rainier-scheduler<br/><i>cron, tasks, without-overlapping</i>]
filesystem[rainier-filesystem<br/><i>local, memory, S3/R2</i>]
session[rainier-session<br/><i>the bag, stores, middleware</i>]
auth[rainier-auth<br/><i>guards, gates</i>]
queue[rainier-queue<br/><i>jobs, drivers, worker</i>]
mail[rainier-mail<br/><i>mailables, transports</i>]
notify[rainier-notify<br/><i>notifications, channels</i>]
broadcast[rainier-broadcast<br/><i>channels, drivers, channel auth</i>]
websocket[rainier-websocket<br/><i>socket handlers, rooms</i>]
metrics[rainier-metrics<br/><i>registry, exposition, timing</i>]
openapi[rainier-openapi<br/><i>the document</i>]
telemetry[rainier-telemetry<br/><i>trace context, OTLP</i>]
server[rainier-server<br/><i>kernel, hyper</i>]
console[rainier-console<br/><i>the console kernel</i>]
framework[rainier-framework<br/><i>facades, bootstrap, prelude</i>]
ormmacros --> orm
orm --> drivers
http --> middleware
middleware --> routing
http --> routing
http --> validation
orm --> database
drivers --> database
events --> database
drivers --> cache
drivers --> filesystem
cache --> scheduler
container --> scheduler
http --> session
middleware --> session
database --> session
cache --> session
crypt --> session
orm --> session
session --> auth
http --> auth
middleware --> auth
database --> auth
orm --> auth
container --> queue
events --> queue
database --> queue
drivers --> queue
orm --> queue
view --> mail
events --> mail
mail --> notify
support --> broadcast
http --> websocket
middleware --> metrics
routing --> openapi
middleware --> telemetry
routing --> server
middleware --> server
container --> server
container --> console
config --> framework
routing --> framework
server --> framework
auth --> framework
queue --> framework
mail --> framework
notify --> framework
broadcast --> framework
websocket --> server
metrics --> framework
openapi --> framework
telemetry --> framework
console --> framework
validation --> framework
cache --> framework
crypt --> framework
session --> framework
scheduler --> framework
filesystem --> framework
support --> framework
classDef umbrella fill:#2d3748,stroke:#4a5568,color:#fff
classDef shared fill:#535,stroke:#a8a,color:#fff
class framework umbrella
class drivers,orm shared
Two crates are shaded because they sit under several others.
rainier-orm is the DBAL itself — a hard fork, vendored rather than
depended on, so the framework and the ORM version together. It has exactly one
dependency, its own derive macro, and no optional ones at all: that is what
keeps it compiling for wasm32 with no feature dance.
rainier-drivers holds transports and nothing else — Redis, Memcached, the
AWS SDKs, and the SQL executors that implement the ORM's Executor port. Redis
is wanted by the cache, the session store and the queue; without a shared home
each would carry its own client, its own protocol code, and its own URL parsing,
so an application would configure Redis three times and open three sets of
connections to one server. The paradigm is written up in that crate's own
module docs: service interfacing lives
in drivers; the port crates are adapters over it.
| Crate | Owns |
|---|---|
rainier-support | Error, Result, BoxFuture, type-maps, string inflection |
rainier-orm | the DBAL: entities, queries, DDL, sharding, the Executor port |
rainier-orm-macros | #[derive(Entity)] |
rainier-container | the IoC container, providers, lifecycle hooks, facades |
rainier-config | the config repository and .env |
rainier-events | the event dispatcher |
rainier-http | requests, responses, cookies, uploads, extractors |
rainier-middleware | the handle(request, next) pipeline, the stack, built-ins |
rainier-http-client | the outbound client, its retry policy and its fake |
rainier-routing | route declaration, groups, resources, URL generation |
rainier-validation | rules, the validator, request contracts |
rainier-view | the template engine |
rainier-database | Rainier ORM integration, models, repositories |
rainier-auth | guards, user providers, gates, abilities, challenges |
rainier-queue | jobs, queue drivers, the worker |
rainier-mail | mailables, the mailer, transports |
rainier-notify | notifications, notifiables, channels |
rainier-broadcast | broadcast channels, drivers, subscription authorisation |
rainier-websocket | the socket handler contract, the socket handle, rooms |
rainier-metrics | the Prometheus registry, its text format, request timing |
rainier-openapi | the OpenAPI document, and rules-to-schema |
rainier-telemetry | W3C trace context, and OTLP behind a feature |
rainier-server | the HTTP kernel and the hyper server |
rainier-console | the console: commands, arguments, the kernel |
rainier-crypt | encryption, signing, key rotation, password hashing |
rainier-session | the session bag, stores, the middleware |
rainier-cache | the cache port, its drivers, and atomic locks |
rainier-scheduler | cron expressions, the schedule, and its locks |
rainier-filesystem | file storage: local, memory, S3/R2 |
rainier-drivers | Redis, Memcached, AWS and SQL transports |
rainier-framework | facades, bootstrap, built-in commands, the prelude |
Applications depend on rainier-framework and get the rest re-exported —
rainier_framework::http, rainier_framework::auth, and so on.
The rules the graph obeys
It is a DAG. No cycles, checked by cargo on every build.
Nothing reaches sideways. routing does not know auth exists. mail
does not know an HTTP request is involved. database does not know a request
is involved either. This is what makes the queue usable in a binary with no
router in it.
The rule holds at the route, which is the place it looks like it should not:
router.get("/me", me).middleware(Authenticate::<User>::resolved_with_guard("api"));
Route::middleware takes impl IntoMiddlewareStack, which bottoms out at
dyn Middleware — a trait in rainier-middleware, which routing already
depends on. The application names Authenticate<User>, because the
application is the crate that knows both the guard and the user model. The
router never names a concrete middleware, so it never needs the crate defining
one.
This used to be a string ("auth:api") resolved through a registry, on the
theory that the dependency was unavoidable otherwise. It was not. See
Middleware.
Ports are traits, adapters are types. Every place the framework talks to the outside world is a trait an application can implement:
| Port | Ships with | Yours would be |
|---|---|---|
Connection | Rainier ORM executors | a driver you wrote |
Cache | memory, Redis, Redis Cluster, Memcached | your own |
SessionStore | memory, database, cache, cookie | Redis Streams, whatever |
Encrypter / Signer | five AEAD ciphers, HMAC, Ed25519 | a KMS |
ViewEngine | TemplateEngine, MemoryEngine | Tera, Askama, whatever |
Transport | LogTransport, FileTransport, MemoryTransport | SMTP, SES, Postmark |
Queue | SyncQueue, MemoryQueue, DatabaseQueue, RedisQueue, SqsQueue | RabbitMQ, NATS |
Guard | TokenGuard, SessionGuard | OAuth, mTLS |
UserProvider | RepositoryUserProvider | LDAP, an upstream API |
Hasher | Argon2Hasher | bcrypt, for legacy rows |
ExceptionRenderer | DefaultExceptionRenderer | your error pages |
Middleware | a dozen built-ins | yours |
Command | four built-ins | yours |
Every component ships a double, and every double refuses to let an
assertion pass vacuously. Dispatcher::fake(), QueueManager::fake(),
Mailer::fake() and MemoryConnection all panic if you assert against a real
instance instead of a recording one — because a test that asserts
assert_nothing_sent() against a live mailer passes for the wrong reason. See
Testing.
What is built on what
Rainier is a framework, not a database library. The ORM underneath it is Rainier ORM, which supplies:
#[derive(Entity)]— table and column metadata from a structrepo::— generic CRUDQuery<E>— a fluent builderExecutor— the port a driver implementsDialect— SQL rendering, viasea-query- shard routing —
ShardRoute,ShardCodec,stable_hash
Rainier adds the framework-shaped layer on top: Model,
Repository, Criteria,
Migrator, lifecycle hook events, and a Connection port
that keeps every future Send. That last one has a story —
Database: the Send story.
HTTP is hyper 1.x with http-body. Templates, validation, the container,
the router and the queue are Rainier's own.
Reading the source
Every module's doc comment explains why it is shaped the way it is, not just what it does. When these pages say "see the module", that is what they mean — the reasoning lives next to the code that made the call, so it stays true when the code changes.
cargo doc --workspace --no-deps --open
This page is the framework’s docs/architecture.md at 7b8cb15.
Found a mistake? Edit it on GitHub.