How this portfolio separates a local state simulation from a private realtime workflow, with explicit ownership, subscription readiness and recovery after an uncertain command.

Connected is not the same as ready.
- 01 / Transport connects
Identity established
The connection token determines the caller. A client argument cannot select another owner.
Not ready for workflow commands - 02 / Caller-filtered view
Initial state arrives
The server resolves the row from the caller identity. The subscription must be applied and its row shape validated.
Readiness begins after subscription - 03 / Explicit command
Server owns transitions
Reducers act on the caller’s row. A failed task may be retried; an uncertain command needs inspection.
No automatic duplicate mutation
Inspect recovery and identity
After an uncertain command, close the connection, preserve the token and reconnect to inspect state. Transport reconnection and the domain’s Retry action solve different problems.
Connections using the same token share a row. Disconnecting preserves it; Clear session removes the row and token only after confirmation.
This describes the configured live path. The local simulation makes no connection and cannot establish server isolation. Connection identity is not a user account or organization role.
Correction, October 1, 2026: the earlier article described a random local counter as a live subscription. It also described visitor analytics that are absent from the current site. This version explains the implemented workflow demo. Live mode requires a configured server and an explicit connection.
One caller, one task
The state experiment gives a visitor one synthetic task to inspect. Choose intake, decision or output, then advance that selected stage through a small state machine. Choosing a stage does not run a three-stage pipeline.
The normal path is queued → running → complete. The decision stage deliberately fails on its first attempt. An explicit retry returns it to queued; the next attempt can complete. Invalid transitions, such as retrying a running task, are rejected.
The useful design question is ownership: which caller may read or change that task? The animation and button labels cannot answer it. The server boundary must.
Keep ownership out of client arguments
The module in spacetimedb/src/index.ts defines a private table keyed by connection identity. This is an excerpt from the implementation, which uses SpacetimeDB 2.10.1:
import { schema, table, t } from "spacetimedb/server";
const workflow_task = table(
{ name: "workflow_task" },
{
owner: t.identity().primaryKey(),
stage: t.string(),
status: t.string(),
attempt: t.u32(),
revision: t.u32(),
},
);
const spacetimedb = schema({ workflow_task });
export default spacetimedb;
The public my_workflow view resolves its row from ctx.sender. It does not accept an owner ID from the browser:
export const my_workflow = spacetimedb.view(
{ name: "my_workflow", public: true },
t.array(workflow_task.rowType),
(ctx) => {
const row = ctx.db.workflow_task.owner.find(ctx.sender);
return row ? [row] : [];
},
);
The start, advance, retry and clear reducers use the same caller identity. A caller can choose a valid stage, but cannot choose whose row a command changes. Keeping that argument out of the API is more reliable than hiding another person's row in the interface.
Connection identity is the ownership boundary for this demo. It is not a user account, an organization role or proof that each browser tab belongs to a different person. Connections using the same token intentionally share the same row.
Share the rules, distinguish the execution
The transition function lives in spacetimedb/shared/workflow.ts. Both the server module and the local simulation use it. The two modes exercise the same allowed transitions, but they establish different things:
Scroll horizontally if needed to read every column.
| Mode | Where state lives | What can be inspected |
|---|---|---|
| Local simulation | The current page's memory | Allowed transitions, deliberate failure and explicit retry. |
| Configured live mode | The server's caller-owned row | Reducer execution and updates delivered through the caller-filtered view. |
The local mode works without a database. It makes no connection and cannot demonstrate server isolation. The live mode needs valid connection settings, a running module and the visitor's Connect to live demo action. The rest of the portfolio remains readable when live mode is unavailable.
Neither mode collects page-view analytics or displays a visitor count. A local state change is shown as a simulation, with no claim that it measures activity elsewhere.
A connection is not yet a usable subscription
The client adapter in lib/realtime/spacetimedb-client.ts receives the generated bindings. It attaches insert, update and delete listeners to myWorkflow, then subscribes only to that generated view.
The adapter marks the session connected after the subscription's onApplied callback, not merely after transport connection. It reads the subscribed rows to update the interface. An invalid row or more than one row closes the session with an error, because either violates this demo's contract.
Connection and initial subscription have a shared deadline. After an interrupted connection, the adapter permits three reconnect attempts with delays of one, two and four seconds. A generation number makes callbacks from abandoned connections ineffective. That prevents an old callback from replacing the state of a newer session.
This is a small implementation detail with a visible consequence: the interface can distinguish connecting, connected, reconnecting and failed instead of displaying a success badge while the required state is still unavailable.
An uncertain command needs inspection
A command timeout does not establish whether the server applied the command. Repeating it automatically could advance this workflow twice.
The adapter therefore separates reconnection from command retry. A reducer call has an eight-second confirmation deadline. If its outcome is uncertain, the client closes the connection, preserves the token and asks the visitor to reconnect and inspect the server's state. It sends no automatic duplicate mutation.
The Retry button has a different meaning: it is an explicit domain action permitted only for a task already in the failed state. Keeping these two kinds of retry separate makes the recovery behavior understandable.
The revision field records state transitions within a run. It is not an idempotency key or a general delivery guarantee; starting a task creates a fresh run with revision zero.
Disconnecting and clearing are different actions
The token is stored in sessionStorage, scoped by server URI and database name. Disconnecting keeps it available for reconnection. It leaves the server row intact.
Clear session first asks the server to delete the caller's row. Only after confirmation does it disconnect and remove the token. If the command cannot be confirmed, the interface preserves the identity so the visitor can reconnect and inspect what happened. Closing the tab is not a request to delete the server row.
One row per identity bounds each caller's state. It does not bound the total number of identities. A public deployment would still need admission controls, retention and resource limits. Those are outside this small demo's implementation.
Test each boundary at its own level
The repository includes separate checks for the transition function, the client adapter and a live local server:
tests/realtime/workflow.test.tsexercises legal transitions, failure, retry and caller-separated model state.tests/realtime/spacetimedb-client.test.tsuses a controlled transport to exercise reconnects, timeouts and stale callbacks.scripts/check-realtime-live.tsexercises generated bindings against a disposable loopback server, including distinct identities and denied access to the private table.
These checks answer different questions. A model test can establish that a transition is rejected; a live integration test is needed to check that the configured server enforces the intended view. Neither establishes production capacity.
You can inspect the local workflow without connecting to a server. The privacy explanation describes what changes when live mode is configured. The small scope is deliberate: ownership, confirmation and recovery remain visible enough to reason about.