Guide

Engines

Choose operational guarantees without changing chart meaning.

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-aware

Use 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:

  1. If the instance may disappear with one Scope or process, use the in-memory tier.
  2. If accepted work must survive process restart, provide durable storage beneath the cluster tier.
  3. If ownership may move among processes, let cluster sharding own the instance.
  4. 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

  1. Classify a temporary modal, a payment workflow, and a local-first editor by the failure each must survive.
  2. Replace durable storage with an in-memory database and explain which engine claim becomes false.
  3. Keep DocumentChart unchanged while adding a second chart to the browser registration batch.