The statechart decides what one valid step means. The engine decides where that step commits, who may advance the instance, and which failures recovery can cross. Choosing an engine is therefore not a choice between modeling features. It is a choice between operational envelopes around the same model.
Start with the failure
Name the loss your application must survive before choosing infrastructure. “Important” is not a failure model; process restart, ownership transfer, browser generation replacement, storage eviction, and device loss are.
In memory
One Layer ScopeUse when losing the Scope or process may also lose the instance.Durable cluster
Process-independentUse when committed work must survive restart or move between process owners.Durable browser
Generation-awareUse when tabs share instances and replacement generations must recover committed work.
The same DocumentChart runs in all three envelopes below. Scroll through them and publish the
document in each frame: Publish still selects the same transition from Draft to Published.
Same chart, different guarantees
Choose the failures an instance survives.
Keep the chart fixed while the operational envelope changes around it. Each frame selects a different owner and recovery boundary for the same Document fold.
1 · Keep work inside one Scope
In memory is a lifecycle, not a prototype.
DocumentChart runs on StatechartEngine.layerMemory. Its registry, instances, mounted work, and retained timeline belong to the Layer Scope and may disappear with that Scope or process.
import { State, Statechart, StatechartEngine, States, Transitions } from "@motive/motive";
import { Layer, Schema } from "effect";
const Publish = Schema.TaggedStruct("Publish", {});
export class Document extends State.Compound<Document>()("Document") {
static states = States.make(() => [Draft, Published]);
}
class Draft extends State.Atomic<Draft>()("Draft") {
static transitions = Transitions.make(this, ({ on }) => [
on(Publish, Published),
]);
}
class Published extends State.Atomic<Published>()("Published") {}
export const DocumentChart = Statechart.make(Document);
export const DocumentLive = DocumentChart.toLayer().pipe(
Layer.provideMerge(StatechartEngine.layerMemory),
);
2 · Cross a process boundary
Durability begins below the engine.
Only the engine Layer changes. Durable statechart and message storage preserve accepted work, while sharding may move one instance's ownership between processes. Publish keeps the same meaning.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { ClusterStatechartEngine } from "@motive/motive-cluster";
import { Layer, Schema } from "effect";
const Publish = Schema.TaggedStruct("Publish", {});
export class Document extends State.Compound<Document>()("Document") {
static states = States.make(() => [Draft, Published]);
}
class Draft extends State.Atomic<Draft>()("Draft") {
static transitions = Transitions.make(this, ({ on }) => [
on(Publish, Published),
]);
}
class Published extends State.Atomic<Published>()("Published") {}
export const DocumentChart = Statechart.make(Document);
export const DocumentLive = DocumentChart.toLayer().pipe(
Layer.provideMerge(ClusterStatechartEngine.layer),
);
3 · Coordinate browser generations
Browser durability ends at browser policy.
BrowserStatechartEngine seals the application's chart batch behind a SharedWorker control plane. Cross-tab ownership and generation recovery still inherit the selected browser storage's limits.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { BrowserStatechartEngine } from "@motive/motive-browser";
import { Effect, Schema } from "effect";
const Publish = Schema.TaggedStruct("Publish", {});
export class Document extends State.Compound<Document>()("Document") {
static states = States.make(() => [Draft, Published]);
}
class Draft extends State.Atomic<Draft>()("Draft") {
static transitions = Transitions.make(this, ({ on }) => [
on(Publish, Published),
]);
}
class Published extends State.Atomic<Published>()("Published") {}
export const DocumentChart = Statechart.make(Document);
const AppCharts = DocumentChart.toLayer();
export const DocumentLive = BrowserStatechartEngine.layer(AppCharts, {
beforeReady: Effect.void,
});
The chart does not choose its lifetime
DocumentChart owns the domain rule: Publish moves Draft to Published. It does not say where
an instance lives, what survives a crash, or who owns concurrent access. Those promises belong to
the engine and the services supplied beneath it.
The first frame uses StatechartEngine.layerMemory. Its registry, instances, timers, activities,
children, and retained timeline belong to the Layer Scope. Closing that Scope or losing the process
loses them. That explicit lifetime is useful for request-local work, tests, previews, and applications
whose state can be reconstructed elsewhere.
Durability begins below the engine
The second frame changes only the engine Layer. ClusterStatechartEngine.layer commits one
macrostep before scheduling timers, activities, and outbound delivery, while sharding gives each
instance one process owner at a time.
That composition becomes durable only when the services beneath it are durable. Statechart storage owns committed chart state; Effect Cluster message storage owns durable messages. Supplying an in-memory implementation makes the whole recovery claim ephemeral, regardless of the engine's name.
The live diagram intentionally uses a local scoped client because this lesson is not a storage conformance test. The adjacent source is the exact typed deployment composition, and the diagram demonstrates the invariant shared by every tier: the chart fold is unchanged.
Browser durability has a platform ceiling
The final frame supplies the application's complete chart-registration Layer to
BrowserStatechartEngine.layer. A SharedWorker coordinates clients and seals one generation so a
replacement can recover against a stable command vocabulary.
That topology can preserve committed state across tabs and generation replacement, but it cannot overrule browser policy. Persistence may be denied, storage may be evicted or deleted, and the device may disappear. “Durable browser” means durable to the named browser failure model, not equivalent to a service-side database.
Durability does not mean exactly once
Recovery preserves committed statechart truth. It does not prove that an external effect happened only once, make an ephemeral database durable, or make browser data immune to deletion. External work needs its own durable identity and idempotency boundary; the engine can then redrive work without confusing another attempt for another intent.
Choose the lightest sufficient tier
Ask what must survive, in order:
- If the instance may disappear with one Scope or process, use the in-memory tier.
- If accepted work must survive process restart, provide durable storage beneath the cluster tier.
- If ownership may move among processes, let cluster sharding own the instance.
- If browser tabs must share instances across generation replacement, use the browser topology and document the remaining platform limits.
The lightest sufficient tier is usually the clearest one: it makes the intended loss boundary visible instead of implying guarantees the application neither needs nor supplies.
Change the world
- Classify a temporary modal, a payment workflow, and a local-first editor by the failure each must survive.
- Replace durable storage with an in-memory database and explain which engine claim becomes false.
- Keep
DocumentChartunchanged while adding a second chart to the browser registration batch.