resonate-state-bus-pattern-typescript — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-state-bus-pattern-typescript (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A long-running Resonate workflow often needs to surface progress to a live UI. If the worker is short-lived (Cloud Function, Lambda) or scales to zero, holding an SSE or WebSocket connection for the workflow's lifetime isn't an option — the connection outlives the worker process.
The state bus pattern decouples the two sides:
ctx.run.The worker stays stateless, progress becomes durable (ctx.run guarantees it), and the UI becomes a pure read view — the workflow can run with or without anyone watching.
Worker (short-lived) Bus Browser (long-lived)
│ │ │
├─ ctx.run(writeState, s1) ──▶ │ │
│ ├─ notify subscribers ───────▶ render(s1)
│ [worker returns, scales to │ │
│ zero — bus persists state] │ │
│ │ │
├─ ctx.run(writeState, s2) ──▶ │ │
│ ├─ notify subscribers ───────▶ render(s2)The workflow never talks to a client. The client never talks to the workflow. Both sides talk to the bus.
example-browser-worker-ts) — then the client is the worker, and in-tab state works.Any durable realtime system with a pub/sub shape works. The only requirement is that the worker can write and the subscriber can listen.
Common options:
| Bus | When it's the right pick |
|---|---|
Firestore (onSnapshot) | GCP-native. One managed service, generous free tier, trivial client SDK. Lightest setup when you're already on GCP. |
| Supabase Realtime | You're already on Supabase; the same table you're writing to can be subscribed to. |
| Pub/Sub + Firestore sink | Higher throughput, fan-out to multiple downstreams. |
| Any DB with change feeds (Mongo, Postgres LISTEN/NOTIFY via a gateway) | Existing infra dictates it. |
The rest of this skill uses Firestore for concreteness. The pattern is identical across buses — only the client libraries change.
Instantiate the bus client at module scope so it's reused across warm invocations. Putting it inside the generator would reconnect on every replay and slow cold paths.
import { Firestore } from "@google-cloud/firestore";
import type { Context } from "@resonatehq/sdk";
const firestore = new Firestore();
const liveDoc = firestore.collection("myapp").doc("live");
async function writeState(_ctx: Context, state: Record<string, unknown>) {
await liveDoc.set(state);
}
function* workflow(ctx: Context) {
// ... compute some state ...
yield* ctx.run(writeState, { step: "ready", t: Date.now() });
// ... more work, more writes ...
}Grant the worker's runtime service account roles/datastore.user on the project (Firestore Native uses Datastore IAM).
import { initializeApp } from "firebase/app";
import { getFirestore, doc, onSnapshot } from "firebase/firestore";
const app = initializeApp({ projectId: "<your-project>" /* + apiKey, etc. */ });
const db = getFirestore(app);
onSnapshot(doc(db, "myapp", "live"), (snap) => {
const state = snap.data();
// render state
});Firestore rules should be read-only for anonymous users on the subscriber collection — no client should ever write through to it. Only the worker's service account does.
One document per workflow, not per event. Overwrite the document on each state write. Subscribers want the current state, not a log. If you need history, write a log alongside the live doc and render it separately.
Idempotent writes. ctx.run replays on failure. The writeState activity must be safe to re-execute with the same input. set() (vs update()) is the simple choice — replays overwrite with identical data.
Shape the state for the UI, not the workflow. The workflow's internal state may be richer than what the UI needs. Project to a narrow DTO in writeState so the subscriber payload stays small and the schema stays stable even as the workflow evolves.
Don't stream high-frequency updates. Firestore (and most buses) bill per write. If your workflow produces 1000 updates/second, batch them — write every N events or every M milliseconds, driven from the workflow.
example-countdown-web-ts and example-browser-worker-ts show a different model: the Resonate worker itself runs inside the browser tab. That's the right shape for a per-tab durable workflow (e.g. a client-side task that must survive a page reload). The state bus pattern is the opposite shape — the worker is server-side, possibly serverless, and the browser is a read-only subscriber. Use the bus pattern when the workflow is shared across viewers or must run when no one is watching.
example-chess-hero-gcp-ts — perpetual chess game as a single Resonate workflow on Cloud Functions Gen 2; move-by-move state written to Firestore; any number of browser tabs subscribe with onSnapshot. Deploy flow covered in resonate-gcp-deployments-typescript (worker) and resonate-server-deployment-cloud-run (server).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.