Choosing Add-ins
On this page
The framework's core — container, router, guards, queue, mail, cache — is
always there. Everything on this page is opt-in: a cargo feature you name,
or nothing of it is compiled. That is a sizing decision as much as a security
one: an application that does not bill does not link a Stripe client, and one
that does not issue tokens has no /oauth/token to misconfigure.
The features
| Feature | Brings in | Page | Example |
|---|---|---|---|
billing-stripe | rainier-billing, http-client | Billing | billing_stripe |
sso | rainier-sso, http-client, jwt | Single Sign-On | sso_login |
accounts | rainier-accounts | Accounts | accounts |
two-factor | rainier-mfa (TOTP); the challenge routes in accounts when it is on | Two-Factor and Passkeys | two_factor |
passkeys | rainier-mfa (WebAuthn); the passkey routes in accounts when it is on | Two-Factor and Passkeys | two_factor |
oauth-server | rainier-oauth-server | OAuth Server | oauth_server |
compat-php | rainier-compat (php::*) | Compatibility | compat_php |
compat-php-oauth | compat-php, oauth-server, and php::oauth | Compatibility | compat_php |
compat-php-webauthn | compat-php, passkeys, and php::webauthn | Compatibility | compat_php |
telemetry | a telemetry feature in every component, and the OTLP exporter | Observability | telemetry |
lookout | rainier-lookout, and the same per-component spans as telemetry without the exporter | Lookout | lookout |
captcha | http-client | Human Checks | captcha |
notify-discord | rainier-notify/discord, http-client | Notifications | notify_discord |
Impersonation needs no feature; it is part of rainier-auth — see
Authentication and the impersonation
example.
Turning one on
[dependencies]
rainier-framework = { version = "2", features = ["accounts", "two-factor", "sso", "telemetry"] }
The rainier facade crate mirrors every one of these names, so rainier = { version = "2", features = [...] } is the same request.
Each add-in is then reached through the framework's re-exports —
rainier_framework::billing, ::sso, ::accounts, ::mfa,
::oauth_server, ::compat — so an application has one dependency to keep
in step, not ten.
Sizing a build
cargo rainier features reads the deployment's environment file and prints
the feature set to build with. Add-ins are named, not inferred, in
RAINIER_ADDINS:
# .env.build — read by the build, not by the running container
RAINIER_ADDINS=accounts,two-factor,sso,billing-stripe
cargo build --release --no-default-features --features "$(cargo rainier features --env .env.build --list)"
Why named: an application often keeps the code for an add-in behind its own
#[cfg(feature = ...)], so finding billing::Stripe in the source says
nothing about whether this deployment bills. Inferring from text would build
everything the repository could do — and an add-in that fails closed at boot
without its keys, like the OAuth server without a signing key, would fail the
deploy. An unknown name is reported rather than ignored. TELEMETRY_ENABLED=true
still asks for telemetry on its own, and LOOKOUT_ENABLED=true for lookout. See
rainier-features.
Running the examples
Every add-in has one, under crates/rainier-framework/examples/, and every
one runs offline: in-memory stores, the HTTP fake standing in for Stripe,
GitHub or Discord, a generated key where a real one would be configured.
cargo run -p rainier-framework --example accounts --features accounts
cargo run -p rainier-framework --example two_factor --features accounts,two-factor,passkeys
cargo run -p rainier-framework --example billing_stripe --features billing-stripe
cargo run -p rainier-framework --example oauth_server --features oauth-server
cargo run -p rainier-framework --example compat_php --features compat-php-oauth,compat-php-webauthn
cargo run -p rainier-framework --example telemetry --features telemetry
cargo run -p rainier-framework --example lookout --features lookout
They are documentation that compiles: CI's clippy --all-targets --all-features builds every one, so an example cannot quietly fall behind the
API it shows. Read the file top to bottom — each prints what it is doing as it
goes.
How they compose
The add-ins are separate crates that meet at the framework's own seams — the guard, the session, the event bus, the cache — rather than at each other. Some combinations are worth knowing:
A product with sign-up, social login and a second factor. accounts +
sso + two-factor (+ passkeys). The accounts routes log users into the
same SessionGuard that sso::login_with logs into, so a user created by
"Sign in with GitHub" and one who registered with a password are the same kind
of row. With two-factor on, a password login for an enrolled user stops at
the challenge; a passkey sign-in has already proven possession and does not.
A paid product. billing-stripe + accounts. Implement Billable on the
user, send Checkout and portal sessions from your controllers, and listen for
the typed webhook events — CustomerSubscriptionUpdated carries a
SubscriptionSnapshot to persist, and that snapshot's valid() is the gate.
The webhook route is outside the session and CSRF middleware; its signature
is its authentication.
A platform others build on. oauth-server + accounts. The server
issues tokens; OAuthGuard accepts them on your API routes, with each
token's scopes arriving as the abilities
that RequireAbility already checks.
Migrating an application to Rainier. compat-php (+ compat-php-oauth,
compat-php-webauthn), usually with telemetry on from the first day. The
two applications share the database, the cache and the keys; Rainier adopts
the other side's sessions, CSRF tokens, OAuth tokens and passkeys as they
arrive, so routes can move across one at a time and users never notice which
side answered. Compatibility has the order to do it in.
Anything in production. telemetry. Set TELEMETRY_ENABLED=true and
OTEL_EXPORTER_OTLP_ENDPOINT, and every request, query, cache call, job,
mail and event is a span, with queue wait and throughput as metrics — no code.
What they share
Every add-in follows the framework's rules, so none of these needs relearning:
- Outbound calls go through the HTTP transport port, so a test hands the
add-in a
FakeTransportand asserts on the exact request — no network, no mock server. - Secrets stay out of
Debugand out of logs. - Refusals fail closed — an unverifiable webhook, an unreachable captcha provider, a passkey with a regressed counter, a token whose row is gone.
- Misconfiguration fails at boot, not on the first request: a feature
switched on without what it needs is an error from
routes()orboot(). - Events, not callbacks, wherever the framework's event bus is in reach —
Registered,TwoFactorFailed,CheckoutSessionCompleted— so auditing, mail and analytics hang off them without the add-in knowing.
This page is the framework’s docs/add-ins.md at 7b8cb15.
Found a mistake? Edit it on GitHub.