Overview
On this page
Rainier is an MVC framework for Rust: a service container, providers that wire it, a router with named routes and middleware groups, form requests, guards, jobs, mailables, facades, events. Each piece sits where an MVC developer expects to find it.
These pages assume you have used an MVC framework before and are new to Rainier. Where Rainier's answer differs from the conventional one, the difference is explained rather than glossed — those are the places you will otherwise waste an afternoon.
Rainier is an independent project. It is not affiliated with, sponsored by, or endorsed by any other framework or its trademark holders, and it claims no rights in their names or marks.
New here? Read Installation, then The Request Lifecycle, then Routing. That is enough to be productive; come back for the rest.
Getting Started
| Page | What it covers |
|---|---|
| Installation | Cloning the starter, prerequisites, and the two dependency rules that bite everyone once |
| Directory Structure | Every directory, and what lives in each |
| Configuration | .env, the config repository, typed keys and driver enums |
| Deployment | What to change before this faces the internet |
Architecture Concepts
| Page | What it covers |
|---|---|
| Architecture Overview | The crates, why they are separate, and the rules they obey |
| The Request Lifecycle | Socket to response, with every seam named |
| Service Container | Binding, resolving, singletons, and what happens on a cycle |
| Service Providers | register and boot, and why they are two phases |
| Facades | Static proxies, how they resolve, and when not to use one |
The Basics
| Page | What it covers |
|---|---|
| Routing | Verbs, parameters, constraints, groups, resources, named routes |
| Middleware | The pipeline, attaching middleware by value, groups, controller middleware |
| Controllers | Actions, extractors, and resource controllers |
| Requests | Input, headers, cookies, uploads, and why bodies are buffered |
| Responses | Builders, IntoResponse, streaming, cookies |
| Views | The template syntax, the engine, and what it deliberately cannot do |
| Vite | Frontend assets: @vite, the dev server, and the built manifest |
| URL Generation | Named routes to URLs, signed links, and the escaping that keeps them safe |
| Validation | Rules, the validator, and request contracts |
| Error Handling | Error, ErrorKind, the exception renderer, 4xx versus 5xx, the debug error page |
Digging Deeper
| Page | What it covers |
|---|---|
| Console | Built-in commands and writing your own |
| Events | The dispatcher, listeners, subscribers, and the fake |
Mailables, envelopes, transports, and always_to | |
| Notifications | A message to a recipient, over their channels — and how it differs from an event |
| Broadcasting | Events pushed to WebSocket channels, and who may subscribe |
| WebSockets | Sockets your own process holds, on the same port as HTTP |
| HTTP Client | Calling somebody else's API, and the fake that makes it assertable |
| Kafka | A partitioned log behind broadcasting, sockets and the queue — and where it disagrees with each |
| Scenarios | Worked designs composing the pages above — starting with a Twitter-shaped feed |
| Queues | Jobs, drivers, the worker, retries, and the reservation protocol |
| Kubernetes jobs | The opt-in path for jobs that need per-job resource sizing and isolation — one-shot batch/v1 Job pods |
| Cache | The cache port, Redis, Redis Cluster, Memcached, Workers KV, atomic locks |
| Filesystem | The storage port, declaring disks, signed URLs, and registering a driver |
| Task Scheduling | Cron expressions, without_overlapping, on_one_server |
| Observability | Prometheus metrics, an OpenAPI document, OpenTelemetry, log format, health checks |
| Helpers | Str inflection, type-maps, the error type, and build_info!() |
Security
| Page | What it covers |
|---|---|
| Authentication | Guards, user providers, the auth middleware, token abilities, challenges |
| Authorization | Gates, policies, actors that are not people, and failing closed |
| Hashing | Argon2id, the timing side channel a login has, and reading hashes you inherited |
| Encryption | Crypt, signing, key rotation, JWTs and a JWKS |
| Sessions | The bag, flash data, drivers, and session fixation |
| Human Checks | Turnstile, hCaptcha and reCAPTCHA tokens verified server-side; failing closed |
Add-ins
Optional crates and modules, each behind a feature of rainier-framework. An
application that does not ask for one compiles none of it. Start with
Choosing add-ins — one table of every feature, what it pulls in,
and the runnable example for each.
| Page | Feature | What it covers | Example |
|---|---|---|---|
| Billing | billing-stripe | Stripe customers, Checkout, the billing portal, subscriptions, payment intents, refunds, verified webhooks on the event bus | billing_stripe |
| Single Sign-On | sso | "Sign in with …" — GitHub, Google, Microsoft, GitLab, Discord, X, any OpenID Connect issuer; PKCE, state, nonce, ID-token verification, account linking | sso_login |
| Accounts | accounts | Registration, login with lockout, logout, password reset, email verification, password confirmation — routes you mount | accounts |
| Two-Factor and Passkeys | two-factor, passkeys | TOTP with replay protection and recovery codes; WebAuthn passkeys; the login challenge when accounts is on | two_factor |
| OAuth Server | oauth-server | Issuing tokens: authorization code with PKCE, refresh rotation, client credentials, password, personal access tokens, scopes, revocation, and the guard that accepts them | oauth_server |
| Compatibility | compat-php, compat-php-oauth, compat-php-webauthn | Running beside the stack you are migrating from: its serialization, encrypted cookies, sessions, cache, CSRF tokens, OAuth tokens and passkeys | compat_php |
| Passive instrumentation | telemetry | A span for every request, query, cache call, job, mail and event; queue wait and throughput metrics; trace context across HTTP and the queue — no application code | telemetry |
| Lookout | lookout | A per-request diagnostics dashboard: every query, cache call, job, outbound call and failure of each request, job and command, with slow-query and N+1 detection — in memory, the database or the cache, behind a gate that answers 404 | lookout |
| Human Checks | captcha | Turnstile, hCaptcha and reCAPTCHA tokens verified server-side | captcha |
| Notifications: Discord | notify-discord | A Discord channel, by webhook or bot | notify_discord |
| Impersonation | — | Acting as another user, with a policy, and routes it is refused on | impersonation |
Run any example with cargo run -p rainier-framework --example <name> --features <feature>.
Database
| Page | What it covers |
|---|---|
| Getting Started | Connections, dialects, executors, raw SQL, and the Send story |
| Models | #[derive(Entity)], the Model trait, route keys, lifecycle hooks |
| Repositories | The Repository contract, EntityRepository, Criteria |
| Relationships | has_many, belongs_to, pivots, and why loading is a call |
| Migrations | The migrator, the schema builder, DDL from model metadata, ordering |
| Pagination | Paginated<T> and what it computes for you |
Testing
Testing — TestApp, factories, the fakes, feature tests against
the real kernel, and the one rule every Rainier double follows.
The concept map
The fastest way in, if you already think in another MVC framework: the concept as you know it, and where it lives here.
| The concept | In Rainier |
|---|---|
| the IoC container | Container |
| the application object | Application |
| service providers | ServiceProvider |
| the application bootstrap file | your bootstrap.rs, built with Rainier |
| the HTTP kernel | Kernel + your app/http/kernel.rs |
| the web routes file | src/routes/web.rs — see Routing |
| declaring a named route | router.get(…).name(…) |
| route middleware by name | .middleware(Authenticate::<User>::resolved()) — by value, not by name |
| resource routes | router.resource — Controllers |
| a model in the action signature | Bound<Post> — implicit route-model binding |
| the authenticated user, injected | AuthenticatedUser<User> as a parameter |
| a form request | FormRequest + Validated<T> |
| reading request input | request.input("x") — Requests |
| the fillable allow-list | falls out of the rules — Validation |
| the template engine | Rainier templates |
| a URL from a named route | urls.route("posts.show", &[("post", slug)]) — URLs |
| an active-record model | #[derive(Entity)] + Model |
| creating / created model hooks | typed hook events |
| the query builder, paginated | Criteria + a repository |
orWhere / a nested where group | or_where(|group| …) |
whereExists / a correlated subquery | Subquery::count(..).correlate(..) |
increment on a column | Assignment::Increment |
| updating some columns by a filter | update_matching / update_column |
upsert with a conflict target | Upsert::on(..) |
soft deletes, withTrashed | #[orm(soft_delete)] |
| a composite primary key | two #[orm(pk)] fields |
filesystems.php and Storage::disk | declaring disks |
Storage::extend | FilesystemDriver::extend |
| a temporary signed URL | temporary_url |
| a read/write connection split | read and write roles, plus sticky |
pool sizing in database.php | a connection's pool |
| more than one cache store | cache.stores |
| has-many / belongs-to | HasMany / BelongsTo, loaded for a slice |
| many-to-many, with a pivot | BelongsToMany |
| eager loading | Post::author().load(&posts, &*users) — one query |
| relation counts without loading | User::posts().count(…) |
| lazy relation access | not available, deliberately |
| migrations | Migrator |
| creating a table | Step::create |
| altering a table | Step::table, with a derived rollback |
| a constrained foreign id | table.foreign_id(..).constrained_on(..) |
writing down() by hand | derived from the change |
| the current user | a guard |
| gate checks | Gate |
| policies | gate abilities over a subject |
| password hashing | Argon2Hasher |
| jobs, and dispatching them | Job + QueueManager |
| mailables | Mailable |
| notifications | Notification |
| notifying a recipient | Notify::instance().send(&user, &…) — Notifications |
via() returning channel names | Channels::with::<C>() — by type |
| one rendering method per channel | three renderings, open channels |
| per-channel recipient addresses | Notifiable::route_for |
| marking an event broadcastable | Broadcastable + a listener |
| a private channel | Channel::private |
| the channel authorisation file | ChannelRegistry — routes/channels.rs |
| the broadcast auth endpoint | broadcasting::authorize::<User> |
| broadcasting to everyone but the sender | event_except |
| sockets held by the framework itself | WebSocketHandler, served on the same port |
| authorising a socket route | authorize, before the handshake |
| events / listeners | Dispatcher |
| facades | facades |
| the console | the console, via cargo run -- |
| the schedule definition | Schedule |
| preventing overlapping runs | .without_overlapping(ttl) |
| running on one server only | .on_one_server() |
| an atomic cache lock | LockManager::lock |
| a Kafka integration | Kafka — as a broadcaster, a relay and a queue |
| a Kafka broadcast driver | KafkaBroadcaster |
| a Kafka consumer command | relay::spawn, in the web process |
| unique jobs | Job::unique_id |
| the mail and queue fakes | the fakes |
| feature-testing an endpoint | TestApp + TestResponse |
| asserting into a JSON body | assert_json_path("a.b", "c") |
| cache remember | cache.remember(key, ttl, closure) |
| raw SQL with bindings | database.query(..).bind(id) |
| a form method override | MethodOverride |
| a queued listener | listen_queued::<E, J>() |
| an execution time limit | Timeout, or SERVER_REQUEST_TIMEOUT |
| log channels and rotation | LOG_FORMAT and whatever collects stdout |
| the outbound HTTP client, and its fake | the same shape |
| a signed route | SignedUrls::route |
| validating a signed link | ValidateSignature::resolved() |
| password confirmation | ConfirmPassword::within(..) |
| subscription billing, a billable model, Stripe webhooks | rainier_billing — feature billing-stripe |
| "sign in with GitHub", a social login driver | Sso::driver("github") — feature sso |
| headless registration / login / reset routes | Accounts — feature accounts |
the verified middleware | EnsureEmailIsVerified |
| token abilities | Abilities + RequireAbility |
| throttling keyed by input | .keyed_by(..).named(..) |
| a model factory | User::factory() |
| rehashing across hash schemes | HashManager |
| a JWKS document | Jwt + JwtKeyRing |
| reading rows a PHP application encrypted | APP_CIPHER=php |
| session access | request.session() |
| flash data | flash data |
| trusted proxies | groups::trust_local_proxies() |
| generating the application key | cargo run -- key:generate |
| an instrumentation dashboard | Lookout per request; metrics and tracing across services |
| generated API documentation | a document generated from the router and the rules |
| cache get / put | the cache |
| encrypting a string | ciphers — five of them |
| public-key signing and sealing | Ed25519 and sealed boxes |
| per-section config files | one module per section in config/ — Configuration |
| controller middleware with only/except | ControllerMiddleware |
| the web middleware group | a function returning a MiddlewareStack — Middleware |
| reading config by key | config.setting(keys::CACHE_DRIVER)? — a typed key, a driver enum |
| encrypt / decrypt via a facade | Crypt::instance().encrypt |
What Rainier does not have
Deliberate gaps, so you do not go looking:
| Feature | Status |
|---|---|
| Localization | not shipped |
| Clustered WebSocket rooms | one process's memory; broadcast through Redis instead |
| Lazy relationship loading | not possible without __get |
has_many_through, polymorphic | two loads and a where_in |
| Queued notifications | queue the job that sends one |
| SAML single sign-on | not shipped — OpenID Connect covers the same identity providers |
| A fillable allow-list | falls out of the rules — Validation |
Design positions
Every one of these is a place where the conventional MVC answer would have produced worse Rust. Each links to the page that explains the trade.
- Middleware is attached by value, never by name. Why
- The auth manager is generic over your user model. Why
- Model hooks can veto but not mutate. Why
- Request bodies are buffered; response bodies stream. Why
- Validation distinguishes absent, null and empty. Why
- A 5xx message never reaches the client outside debug. Why
- A notification declares three renderings, not one method per channel. Why
- A relationship is loaded for a slice, never for one model. Why
- A migration's
downis derived from itsup, not written twice. Why - An OpenAPI request body is the validator's own rules. Why
- Nothing is autoloaded and nothing is discovered by name. Why
- A setting the framework cannot honour is refused, never ignored. Why
- Two declarations of one default fail the boot rather than resolving by precedence. Why
- A correlated subquery cannot be constructed without its correlation. Why
- A soft-delete column is declared, never inferred from its name. Why
- An upsert refuses a conflict target the insert does not supply. Why
- A disk that cannot sign a temporary URL fails instead of returning a public one. Why
This page is the framework’s docs/overview.md at 7b8cb15.
Found a mistake? Edit it on GitHub.