Lifetimes & Navigation
How device work is owned by a module activation, what happens on navigation, deactivation and reconnect, and why the background lifetime is reserved
Lifetimes & Navigation
Every device operation has an owner. By default the owner is the exact activation of the module instance that started it. When the user leaves the screen, the server cancels the operation, and a late result is thrown away instead of uploaded.
Activation lifetime (the default)
A module instance is activated each time it takes the active route slot, and deactivated when it loses it. See the module lifecycle. Device authority follows those transitions exactly:
| Moment | Device calls |
|---|---|
onCreated, before the first activation | Return unavailable (owner-inactive) at once, instead of waiting for activation |
onActivated | Work. Authority begins just before onActivated runs. |
| Action handlers while active | Work |
onDeactivated, onDestroyed | Return unavailable (owner-inactive). Authority is revoked before these run. |
When a module deactivates, the server cancels every operation owned by that
activation, including pending pickers, open streams and recordings. Those
operations settle as cancelled. The device drops the work, and anything
that finishes later is discarded.
Under ManagedRouter, route modules persist across navigation
by default. When the user navigates back, the instance is reused and
onActivated runs again with a new activation. Operations from the
earlier visit are gone. Coming back doesn't bring them back.
export default app
.defineState({ camera: "unknown" as string })
.onActivated(async (state, context) => {
// Runs on every visit. Device calls work here.
const res = await context?.device.permissions.query("camera");
state.camera = res?.ok ? res.value.status : "unknown";
})
.onCreated(async (state, context) => {
// Too early: the module isn't active yet, so this returns unavailable.
// Put device calls in onActivated instead.
})
.build();Awaits and timers keep the authority they started with
A handler holds on to the activation that was live when it started. If an
await resumes after the module has deactivated, new device calls from
that continuation fail with unavailable (owner-inactive). This holds even
if the module has been activated again since. A stale continuation can't
start work on behalf of a newer visit.
The same applies to timers and callbacks that captured the context. They keep working while the activation that created them is live, and stop afterwards.
Unary requests end with the handler
Separately from activation, a unary request (request(), save()) is
cancelled if the handler that started it returns while the request is
still pending. That's why every device call should be awaited inside its
handler. Streams aren't tied to the handler this way. They run until they
settle, you cancel them, or the activation ends. See
Requests & Streams.
In the Rust SDK
Rust handlers are synchronous, so there is no await to keep a handler
waiting: a unary call returns a DeviceCall, and consuming it (then,
on_settled, wait) is what keeps the request alive past the handler.
A DeviceCall dropped unconsumed is cancelled at once, the equivalent of
the rule above. The Device a handler got keeps that invocation's
activation: if the module has been deactivated since, calls through it fail
unavailable (owner-inactive).
RemoteSession gives the primary module and every nested module one
activation that lasts as long as the connection. A host that moves modules on
and off screen itself calls session.deactivate_module(name) (which cancels
that activation's work) and session.activate_module(name) (which starts a
new activation); unregister_module(name) destroys the module and sweeps all
of its device work.
Background lifetime (reserved)
The protocol defines a background lifetime, owned by the module instance
instead of one activation, for work that should outlive navigation, such as a
long recording. It is not available in revision 1: every capability
allows activation only, so a request with lifetime: "background" returns
unsupported.
The server already enforces the rules a future background capability would need:
- Destroying the module cancels its background work, but ordinary deactivation doesn't.
- At most 2 modules per connection may hold background work at once.
Requests over that cap are
throttled. - The device must show a compliant background indicator. The web host never admits background work.
Connection loss
Device work is scoped to the connection and is never resumed:
- If the socket closes, every pending operation fails with
connectionLost, and the device tears down its dialogs, recordings and temporary files. - The server renews every running operation every 5 seconds. If no renewal
is acknowledged for 15 seconds, both sides stop the operation
(
connectionLost). Renewals keep a request alive even when no data flows, for example while a picker waits for the user. - After a reconnect, the client advertises its capabilities again, and new requests start fresh. An operation is never replayed or resumed. App state can be persisted and restored (see Persistence), but device operations can't.
- On Cloudflare, a Durable Object that wakes up without its in-memory broker
closes the socket with code
1012, and the client reconnects.
Replayed dispatches
Device work can only start from a dispatch that the connection's own client
sent. A dispatch replayed to other clients by syncActions, or derived from a
broadcast, gets a device context that always returns unavailable
(syncActions.replay). The restriction survives await. A server that uses
syncActions keeps its device plane: the client that dispatched the action
can start device work, and the replayed copies can't.
Errors
The ten device error codes, when each occurs, the platformDetail values you will see, and how to handle errors in each SDK
Consent & Security
How the device decides — host-owned consent, activity indicators, cooldowns and one-prompt-at-a-time, connection admission with Origin allowlists and authenticators, and what the server may and may not trust