bootstrap.rs is the one place where your application wires itself up
at startup. Container bindings, event listeners, observers, supervisors,
global middleware - anything that should exist before the first request
hits the server (or the first job pops off the queue) is registered
inside a single async bootstrap function. There is no
service-provider scaffold to assemble; one function, run once, is the
whole API.
The shape
A scaffolded app's entry point builds an Application
fluently and runs it. The bootstrap step is one method on the
builder:
// cmd/main.rs
use ;
use Application;
async
#[suprnova::main], not #[tokio::main]
The attribute is not cosmetic, and swapping it back breaks the boot with a message explaining why.
Loading .env writes to the process environment, and set_var is sound
only while the process is single-threaded. #[tokio::main] builds the
runtime around the whole of main, so every worker thread already
exists before your first statement runs - and any of them can call
getenv indirectly through DNS resolution, time formatting, or a C
dependency. The race is silent when it goes wrong, which is the worst
property a race can have.
#[suprnova::main] keeps the same async fn main you would write
anyway, and simply reorders two things: it loads the environment, then
builds the runtime, then runs your body on it. It accepts the same
flavor and worker_threads arguments as #[tokio::main].
If Application::run finds the environment was never loaded from a
single-threaded context, it refuses to boot rather than warning - an app
that starts "fine" under #[tokio::main] is precisely the one that
corrupts an unrelated environment read weeks later.
The framework calls your bootstrap_fn once during the boot sequence,
after the environment is loaded and after the runtime drivers (Cache, Queue,
RateLimit, Mail) are up but before the router is built. The same call
runs for background workers (queue:work, workflow:work,
schedule:work) so an observer or listener registered here fires
identically for an insert from a queue job and an insert from an HTTP
handler. Lifecycle walks the full sequence.
The function's signature is fixed by Application::bootstrap:
// src/bootstrap.rs
pub async
It returns (). Fallible setup uses .expect("…") with a message that
explains the remediation - boot is the right time to fail loudly. The
example app's call is DB::init().await.expect("Failed to connect to database"); so a missing DATABASE_URL aborts the process at boot
with the actual error printed, instead of surfacing as a confusing
"connection refused" on the first request.
What goes in bootstrap
A real bootstrap function does a small number of distinct things.
Each subsection below is one of them. The example app's
app/src/bootstrap.rs exercises all of them and is the working
reference.
Database connection
use DB;
pub async
DB::init reads DatabaseConfig (registered by your config_fn) and
opens the pool. The connection is stored in the container
as a singleton - DB::connection() / DB::get() resolves it
anywhere. DB::init_with(config) is the test-and-tooling escape
hatch when you want to point at something other than the env-derived
URL.
Global middleware
use ;
use cratemiddleware;
pub async
global_middleware! registers a layer that runs on every request,
including unrouted ones (404s, OPTIONS preflight). The order you
register in is the order the chain runs - outside-in. The framework
slots its own RequestIdMiddleware outermost; everything you add sits
inside it. Middleware explains the full chain shape,
including the per-route layer.
Container bindings
The container takes whatever you put in it; the macros are sugar over
the App facade.
use Arc;
use ;
use crateDatabaseUserProvider;
pub async
Trait-object bindings are the most common shape - bind an interface,
let handlers and tests substitute the implementation. The
Container chapter has the full binding API including
bind_factory!, the _if_absent variants, and the three-layer
lookup model.
Event listeners and observers
The dispatcher is alive as soon as bootstrap runs - listeners registered here see every subsequent dispatch.
use Arc;
use EventFacade;
use crateUserRegistered;
use crateSendWelcomeEmailListener;
pub async
Eloquent observers (#[suprnova::observer(M)]) collect themselves via
inventory::submit! at compile time. One call drains the inventory
into the dispatcher:
bootstrap_observers
.await
.expect;
The call is idempotent - re-running bootstrap (a worker that boots a second time) does not double-register the listener adapters. Events covers dispatch and listener authoring; Eloquent covers observers.
Supervisors
Long-running background tasks declared via the Supervisor trait and
inventory::submit! start through one call:
use SupervisorRegistry;
pub async
Each supervisor runs in its own restart-loop task with a panic boundary; a panicked supervisor is logged and restarted, not allowed to take the process down. See Supervisors for the trait and the restart policy.
Worker job registration
Queue jobs and mailables that workers need to dispatch by name register themselves at boot:
use register_job;
pub async
Without this, the worker has no way to map a queued envelope back to the type that handles it.
The post-boot hook: booted()
Bootstrap registers; booted() resolves. The builder takes a
second callback that fires after the server has finished its own
service boot but before it begins accepting connections. Use it when
you need to read something the framework itself bound during boot:
new
.config
.bootstrap
.routes
.booted
.run
.await;
booted is synchronous and runs after Server::from_config - drivers
are up, encryption keys are loaded, your bindings exist. Most apps do
not need this hook; reach for it when a one-shot post-boot side effect
needs to see a fully-constructed container.
A complete bootstrap.rs
A trimmed but representative shape, drawn from the example app:
//! Application bootstrap - register services, listeners, and
//! global middleware.
use Arc;
use Duration;
use ;
use ;
use register_job;
use ;
use crateChatChannel;
use crateUserRegistered;
use crateSendWelcomeEmailListener;
use cratemiddleware;
use crateDatabaseUserProvider;
pub async
Notice the rhythm: each block does one thing, calls one or two APIs, and either succeeds or fails with a clear message. Nothing here is clever; the function is long because the app has a lot of moving parts, not because the bootstrap pattern is complicated.
When to bootstrap vs #[injectable]
#[injectable] is a macro that auto-registers a singleton in the
container's inventory at compile time. It is the right choice for
services that need nothing more than their #[inject] dependencies to
construct:
use injectable;
;
These resolve themselves; bootstrap does not need to touch them.
Bootstrap is the right place when construction needs anything else -
an environment variable, a constructed config struct, a dyn Trait
binding, a runtime decision, an async setup call, or registration of
something that is not itself a service (a listener, an observer, a
queue job mapping, a global middleware layer).
Use #[injectable] for |
Use bootstrap for |
|---|---|
| Concrete singletons with no runtime config | Anything dyn Trait |
| Services constructed from other injectables | Anything async at boot |
| Default DI graph | Environment-driven values |
| Event listeners, observers, supervisors | |
| Global middleware | |
| Worker job + mailable registration |
You can mix freely. #[injectable] services are visible in the
container by the time bootstrap runs, so a binding in bootstrap can
read them.
Where bootstrap sits in the boot order
The full sequence (excerpted from Lifecycle):
Config::init(".")- load.env, detect environmentinit_policies()- drain the#[policy]inventory- Your
config_fnruns (typed config registration) - Migrations run (auto-migrate on
serve) - Your
bootstrap_fnruns ←bootstrap::register - Routes assembled from your
routes_fn Server::from_configboots drivers + container- Your
booted_fns fire - Server begins accepting connections
Background workers (queue:work, workflow:work, schedule:work)
share steps 1–5 and 7 so a listener or observer you register reaches
worker code paths exactly as it reaches HTTP handlers.
Why Suprnova diverges
Laravel splits boot across multiple service providers: each provider
implements register() and boot(), they're collected in
config/app.php, and Laravel walks them in two passes (all register,
then all boot) so a service can depend on another provider's
bindings without ordering ceremony in user code. The provider class
gives you a unit of organisation when an app accumulates dozens of
distinct subsystems.
Suprnova collapses that to one function. The reasons:
- The two-pass
register/bootsplit solves an ordering problem Rust does not have.#[injectable]and the container'sbootstrap_singletonsalready resolve dependency graphs without user-visible ordering. Bindings register inline; the lookup machinery handles the rest. - One function is easier to read than ten. A new contributor
opens
bootstrap.rsand sees every binding, every listener, every observer, every middleware layer in one place. Provider-style fragmentation hides what the app actually does. - Inventory-style auto-registration covers the rest. Observers,
supervisors, scheduled tasks, policies, and queue handlers all
collect themselves at compile time via
inventory::submit!. Bootstrap drains the inventories with single calls (bootstrap_observers,SupervisorRegistry::start_all) rather than enumerating each.
Where Laravel earns the provider split is library distribution: a
crate that ships its own bindings would want a registration entry
point that an app can opt into without editing its own bootstrap.
Suprnova's analogue is a public pub async fn register() in the
crate's root and a one-line call from the app's bootstrap. The
ergonomic cost is one line; the readability gain is everything in
one place.
Next
- Lifecycle - full boot order and where
bootstrap_fnfires - Container -
App::bind/App::singleton/App::factoryand the three-layer lookup - Configuration - typed config registration that runs before bootstrap
- Middleware - chain composition for layers
registered with
global_middleware! - Events - the dispatcher that listeners and observers plug into
