Overview

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

PageWhat it covers
InstallationCloning the starter, prerequisites, and the two dependency rules that bite everyone once
Directory StructureEvery directory, and what lives in each
Configuration.env, the config repository, typed keys and driver enums
DeploymentWhat to change before this faces the internet

Architecture Concepts

PageWhat it covers
Architecture OverviewThe crates, why they are separate, and the rules they obey
The Request LifecycleSocket to response, with every seam named
Service ContainerBinding, resolving, singletons, and what happens on a cycle
Service Providersregister and boot, and why they are two phases
FacadesStatic proxies, how they resolve, and when not to use one

The Basics

PageWhat it covers
RoutingVerbs, parameters, constraints, groups, resources, named routes
MiddlewareThe pipeline, attaching middleware by value, groups, controller middleware
ControllersActions, extractors, and resource controllers
RequestsInput, headers, cookies, uploads, and why bodies are buffered
ResponsesBuilders, IntoResponse, streaming, cookies
ViewsThe template syntax, the engine, and what it deliberately cannot do
ViteFrontend assets: @vite, the dev server, and the built manifest
URL GenerationNamed routes to URLs, signed links, and the escaping that keeps them safe
ValidationRules, the validator, and request contracts
Error HandlingError, ErrorKind, the exception renderer, 4xx versus 5xx, the debug error page

Digging Deeper

PageWhat it covers
ConsoleBuilt-in commands and writing your own
EventsThe dispatcher, listeners, subscribers, and the fake
MailMailables, envelopes, transports, and always_to
NotificationsA message to a recipient, over their channels — and how it differs from an event
BroadcastingEvents pushed to WebSocket channels, and who may subscribe
WebSocketsSockets your own process holds, on the same port as HTTP
HTTP ClientCalling somebody else's API, and the fake that makes it assertable
KafkaA partitioned log behind broadcasting, sockets and the queue — and where it disagrees with each
ScenariosWorked designs composing the pages above — starting with a Twitter-shaped feed
QueuesJobs, drivers, the worker, retries, and the reservation protocol
Kubernetes jobsThe opt-in path for jobs that need per-job resource sizing and isolation — one-shot batch/v1 Job pods
CacheThe cache port, Redis, Redis Cluster, Memcached, Workers KV, atomic locks
FilesystemThe storage port, declaring disks, signed URLs, and registering a driver
Task SchedulingCron expressions, without_overlapping, on_one_server
ObservabilityPrometheus metrics, an OpenAPI document, OpenTelemetry, log format, health checks
HelpersStr inflection, type-maps, the error type, and build_info!()

Security

PageWhat it covers
AuthenticationGuards, user providers, the auth middleware, token abilities, challenges
AuthorizationGates, policies, actors that are not people, and failing closed
HashingArgon2id, the timing side channel a login has, and reading hashes you inherited
EncryptionCrypt, signing, key rotation, JWTs and a JWKS
SessionsThe bag, flash data, drivers, and session fixation
Human ChecksTurnstile, 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.

PageFeatureWhat it coversExample
Billingbilling-stripeStripe customers, Checkout, the billing portal, subscriptions, payment intents, refunds, verified webhooks on the event busbilling_stripe
Single Sign-Onsso"Sign in with …" — GitHub, Google, Microsoft, GitLab, Discord, X, any OpenID Connect issuer; PKCE, state, nonce, ID-token verification, account linkingsso_login
AccountsaccountsRegistration, login with lockout, logout, password reset, email verification, password confirmation — routes you mountaccounts
Two-Factor and Passkeystwo-factor, passkeysTOTP with replay protection and recovery codes; WebAuthn passkeys; the login challenge when accounts is ontwo_factor
OAuth Serveroauth-serverIssuing tokens: authorization code with PKCE, refresh rotation, client credentials, password, personal access tokens, scopes, revocation, and the guard that accepts themoauth_server
Compatibilitycompat-php, compat-php-oauth, compat-php-webauthnRunning beside the stack you are migrating from: its serialization, encrypted cookies, sessions, cache, CSRF tokens, OAuth tokens and passkeyscompat_php
Passive instrumentationtelemetryA 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 codetelemetry
LookoutlookoutA 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 404lookout
Human CheckscaptchaTurnstile, hCaptcha and reCAPTCHA tokens verified server-sidecaptcha
Notifications: Discordnotify-discordA Discord channel, by webhook or botnotify_discord
Impersonation—Acting as another user, with a policy, and routes it is refused onimpersonation

Run any example with cargo run -p rainier-framework --example <name> --features <feature>.

Database

PageWhat it covers
Getting StartedConnections, dialects, executors, raw SQL, and the Send story
Models#[derive(Entity)], the Model trait, route keys, lifecycle hooks
RepositoriesThe Repository contract, EntityRepository, Criteria
Relationshipshas_many, belongs_to, pivots, and why loading is a call
MigrationsThe migrator, the schema builder, DDL from model metadata, ordering
PaginationPaginated<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 conceptIn Rainier
the IoC containerContainer
the application objectApplication
service providersServiceProvider
the application bootstrap fileyour bootstrap.rs, built with Rainier
the HTTP kernelKernel + your app/http/kernel.rs
the web routes filesrc/routes/web.rs — see Routing
declaring a named routerouter.get(…).name(…)
route middleware by name.middleware(Authenticate::<User>::resolved()) — by value, not by name
resource routesrouter.resource — Controllers
a model in the action signatureBound<Post> — implicit route-model binding
the authenticated user, injectedAuthenticatedUser<User> as a parameter
a form requestFormRequest + Validated<T>
reading request inputrequest.input("x") — Requests
the fillable allow-listfalls out of the rules — Validation
the template engineRainier templates
a URL from a named routeurls.route("posts.show", &[("post", slug)]) — URLs
an active-record model#[derive(Entity)] + Model
creating / created model hookstyped hook events
the query builder, paginatedCriteria + a repository
orWhere / a nested where groupor_where(|group| …)
whereExists / a correlated subquerySubquery::count(..).correlate(..)
increment on a columnAssignment::Increment
updating some columns by a filterupdate_matching / update_column
upsert with a conflict targetUpsert::on(..)
soft deletes, withTrashed#[orm(soft_delete)]
a composite primary keytwo #[orm(pk)] fields
filesystems.php and Storage::diskdeclaring disks
Storage::extendFilesystemDriver::extend
a temporary signed URLtemporary_url
a read/write connection splitread and write roles, plus sticky
pool sizing in database.phpa connection's pool
more than one cache storecache.stores
has-many / belongs-toHasMany / BelongsTo, loaded for a slice
many-to-many, with a pivotBelongsToMany
eager loadingPost::author().load(&posts, &*users) — one query
relation counts without loadingUser::posts().count(…)
lazy relation accessnot available, deliberately
migrationsMigrator
creating a tableStep::create
altering a tableStep::table, with a derived rollback
a constrained foreign idtable.foreign_id(..).constrained_on(..)
writing down() by handderived from the change
the current usera guard
gate checksGate
policiesgate abilities over a subject
password hashingArgon2Hasher
jobs, and dispatching themJob + QueueManager
mailablesMailable
notificationsNotification
notifying a recipientNotify::instance().send(&user, &…) — Notifications
via() returning channel namesChannels::with::<C>() — by type
one rendering method per channelthree renderings, open channels
per-channel recipient addressesNotifiable::route_for
marking an event broadcastableBroadcastable + a listener
a private channelChannel::private
the channel authorisation fileChannelRegistry — routes/channels.rs
the broadcast auth endpointbroadcasting::authorize::<User>
broadcasting to everyone but the senderevent_except
sockets held by the framework itselfWebSocketHandler, served on the same port
authorising a socket routeauthorize, before the handshake
events / listenersDispatcher
facadesfacades
the consolethe console, via cargo run --
the schedule definitionSchedule
preventing overlapping runs.without_overlapping(ttl)
running on one server only.on_one_server()
an atomic cache lockLockManager::lock
a Kafka integrationKafka — as a broadcaster, a relay and a queue
a Kafka broadcast driverKafkaBroadcaster
a Kafka consumer commandrelay::spawn, in the web process
unique jobsJob::unique_id
the mail and queue fakesthe fakes
feature-testing an endpointTestApp + TestResponse
asserting into a JSON bodyassert_json_path("a.b", "c")
cache remembercache.remember(key, ttl, closure)
raw SQL with bindingsdatabase.query(..).bind(id)
a form method overrideMethodOverride
a queued listenerlisten_queued::<E, J>()
an execution time limitTimeout, or SERVER_REQUEST_TIMEOUT
log channels and rotationLOG_FORMAT and whatever collects stdout
the outbound HTTP client, and its fakethe same shape
a signed routeSignedUrls::route
validating a signed linkValidateSignature::resolved()
password confirmationConfirmPassword::within(..)
subscription billing, a billable model, Stripe webhooksrainier_billing — feature billing-stripe
"sign in with GitHub", a social login driverSso::driver("github") — feature sso
headless registration / login / reset routesAccounts — feature accounts
the verified middlewareEnsureEmailIsVerified
token abilitiesAbilities + RequireAbility
throttling keyed by input.keyed_by(..).named(..)
a model factoryUser::factory()
rehashing across hash schemesHashManager
a JWKS documentJwt + JwtKeyRing
reading rows a PHP application encryptedAPP_CIPHER=php
session accessrequest.session()
flash dataflash data
trusted proxiesgroups::trust_local_proxies()
generating the application keycargo run -- key:generate
an instrumentation dashboardLookout per request; metrics and tracing across services
generated API documentationa document generated from the router and the rules
cache get / putthe cache
encrypting a stringciphers — five of them
public-key signing and sealingEd25519 and sealed boxes
per-section config filesone module per section in config/ — Configuration
controller middleware with only/exceptControllerMiddleware
the web middleware groupa function returning a MiddlewareStack — Middleware
reading config by keyconfig.setting(keys::CACHE_DRIVER)? — a typed key, a driver enum
encrypt / decrypt via a facadeCrypt::instance().encrypt

What Rainier does not have

Deliberate gaps, so you do not go looking:

FeatureStatus
Localizationnot shipped
Clustered WebSocket roomsone process's memory; broadcast through Redis instead
Lazy relationship loadingnot possible without __get
has_many_through, polymorphictwo loads and a where_in
Queued notificationsqueue the job that sends one
SAML single sign-onnot shipped — OpenID Connect covers the same identity providers
A fillable allow-listfalls 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 down is derived from its up, 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.

Type to search 2.0.0 docs.