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
here. There is no service-provider scaffold to assemble.
There are two hooks, not one. register is process-wide: every
subcommand runs it, including queue:work, schedule:work,
workflow:work, and your console binary, not only the server. Register
the database connection, container bindings, event listeners, observers,
supervisors, and worker job registration there. register_http_stack,
wired through .http_bootstrap, runs only on the server path (serve /
web:run) - global middleware and Inertia::install belong there. The
"Where bootstrap sits in the boot order" section below explains why the
split exists.
The shape
A scaffolded app's entry point builds an Application
fluently and runs it. Bootstrap is two methods 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. http_bootstrap_fn runs immediately after bootstrap_fn, but
only on the server path - background workers and the console binary
never call it. Lifecycle walks the full sequence.
Both functions' signatures are fixed by Application::bootstrap and
Application::http_bootstrap:
// src/bootstrap.rs
pub async
register returns (); register_http_stack is synchronous, not
async - both are wired as async closures at the call site
(.http_bootstrap(|| async { bootstrap::register_http_stack() }))
because a plain function pointer can also serve as a test harness
entry point without pulling async into the test. 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. The
subsections below describe bootstrap responsibilities available to an
application; the example app exercises most, but not all, of them. For current
default Magnetar initialization, use the API scaffold's src/bootstrap.rs
template as 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.
Magnetar authentication engine
Applications using the built-in password, passkey, magic-link, bearer,
lockout, remember, or OAuth facades initialize Magnetar after the database and
APP_KEY are ready:
use ;
pub async
The default MagnetarConfig binds application identities to the canonical
app_users table. The generated full-stack scaffold uses a users model and
does not initialize Magnetar, so do not add the default initializer unchanged
to that scaffold. Use the API scaffold's app_users model, or construct a
custom MagnetarHostEngine and AuthSchema binding for your existing users
table. Keep the framework UserProvider and Magnetar host binding on the same
application identity. The API scaffold, not app/src/bootstrap.rs, is the
current working reference for default MagnetarConfig initialization.
Magnetar is process-wide because queue workers, schedulers, HTTP handlers, and
session middleware use the same credential and session stores. Put
init_magnetar in register, not register_http_stack. The installer is
one-shot and fails if another engine is already installed.
The API scaffold reads PASSKEY_RP_ID and PASSKEY_RP_ORIGIN in application
bootstrap. Those names are scaffold conventions rather than framework-owned
environment variables.
Global middleware
Global middleware is HTTP-only, so it belongs in register_http_stack,
not register:
use ;
use cratemiddleware;
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
.http_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
This representative composition is not a verbatim excerpt from the example
app. It keeps process-wide registration in register and HTTP-only setup in
register_http_stack. Magnetar initialization is shown separately above
because its application-user schema must match the framework user provider.
//! Application bootstrap - register services, listeners, global
//! middleware, and the Inertia layer.
use Arc;
use Duration;
use ;
use ;
use register_job;
use ;
use crateChatChannel;
use crateUserRegistered;
use crateSendWelcomeEmailListener;
use cratemiddleware;
use crateUser;
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 functions are 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 - Your
http_bootstrap_fnruns, server path only ←bootstrap::register_http_stack - 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) and
the console binary share steps 1-5 and 8 - they run bootstrap_fn, but
never step 6, since only serve / web:run runs http_bootstrap_fn.
That is what lets a listener or observer you register in register
reach worker code paths exactly as it reaches HTTP handlers, while
register_http_stack's global middleware and Inertia::install stay
off processes that never serve HTTP.
Why Suprnova diverges
Laravel runs every service provider's register() and boot() for
artisan commands and queue workers too, not only for HTTP requests -
and gets away with it because its Vite integration resolves asset URLs
lazily, at render time, from whatever the @vite Blade directive is
asked to render. A worker that never renders a view never touches the
manifest, so a missing build simply never comes up.
Suprnova's Inertia::install resolves the manifest once, at boot, and
fails closed in production when it is missing - by design, so a
misconfigured deployment cannot serve asset URLs pointing at a Vite dev
server nobody runs. That design choice is exactly what breaks a worker
or console image that (correctly) ships no public/assets: the failure
Laravel defers to request time, Suprnova would otherwise hit at process
start, on every subcommand. Splitting the boot surface into bootstrap
and http_bootstrap keeps the fail-closed check, but only where it
belongs - the server path that will actually render an Inertia page.
Laravel also splits boot itself 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 two functions - register and
register_http_stack - rather than a register/boot pair per
provider. 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. - Two functions are easier to read than ten. A new contributor
opens
bootstrap.rsand sees every binding, every listener, every observer, every middleware layer in one of two places. 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
