Durable execution usually means new infrastructure: an orchestrator to run, a queue to feed it, a scheduler to wake things up. resonate-pg collapses all of that into the database you already have. It is the Resonate Server implemented as one SQL file — 1,351 lines of PL/pgSQL — and any Postgres 16+ with pg_cron becomes a durable execution engine when you apply it.
On Supabase, a stock project already has everything the system needs:
- Postgres runs the engine. Durable promises, tasks, timers, and cron schedules are rows. pg_cron is the only clock. pg_net is the only transport.
- Edge Functions are the workers. The database invokes them over HTTP. They checkpoint each step back to Postgres and exit. They hold no state.
- Realtime streams workflow progress to the browser, and auth, storage, and vectors are in the same project.
No orchestrator service, no queue, no scheduler, no second database. We set it up from scratch, ran it, crashed it on purpose, and measured it.
Five minutes to a durable workflow
The whole installation, from nothing:
supabase projects create my-durable-app
supabase link --project-ref <ref>
supabase db query --linked "create extension if not exists pg_cron; create extension if not exists pg_net;"
supabase db query --linked -f resonate.sql # ← the entire server. 2.7 seconds.
supabase functions deploy countdown --no-verify-jwt
--no-verify-jwt is required because pg_net can't attach an auth header. Forged task messages are rejected against the database, but the endpoint is publicly reachable — add a shared-secret header check in your handler before production.
Then write an ordinary-looking function with the TypeScript SDK:
import { type Context, Resonate } from "jsr:@resonatehq/supabase@0.4.1";
const resonate = new Resonate();
resonate.register("countdown", async function countdown(ctx: Context, n: number) {
for (let i = n; i > 0; i--) {
await ctx.run((ctx: Context) => broadcast(ctx.originId, `countdown: ${i}`)); // checkpointed step
await ctx.sleep(60_000); // durable: no process waits
}
await ctx.run((ctx: Context) => broadcast(ctx.originId, "liftoff 🚀"));
});
resonate.httpHandler();
And start it from SQL:
select resonate.invoke('countdown-1', 'countdown', '[3]'::jsonb,
'https://<ref>.functions.supabase.co/countdown');
ctx.run checkpoints each step to Postgres as it completes. ctx.sleep writes a row with a deadline and returns — the invocation ends, and for the next sixty seconds (or seven days) the workflow exists only as that row. pg_cron resolves the timer and pg_net wakes a fresh invocation, which replays completed steps from the database and continues. Sleeps and timeouts resolve on the next 5-second engine tick — it's a scheduler, not a stopwatch.
The numbers
Everything below was measured on a hosted free-tier Supabase project (us-west-1), not a tuned box.
| What | Measured |
|---|---|
Apply resonate.sql (server install) |
2.7 s |
invoke → first checkpointed step running on an Edge Function |
0.9–1.2 s |
Checkpoint overhead per ctx.run step |
~17 ms average |
| 25-way fan-out (each child its own invocation, parent suspended) | 4.0 s end to end |
| Durable sleep wake-up | deadline + ~2.5 s (the engine ticks every 5 s) |
Cron schedule (* * * * *) |
fired at :00.000 of each minute, runs resolved sub-second |
| Raw engine cost, server-side | ~0.27 ms per protocol op (~3,600 promise-creates/s on one connection) |
The engine itself costs ~0.27 ms per protocol op — the observable latencies are Supabase plumbing: the pg_net HTTP push, Edge Function startup, and the 5-second timer tick.
We killed the worker on purpose
The claim that matters is crash recovery, so we forced the failure. A workflow checkpoints step A, then busy-loops until Supabase's runtime hard-kills the worker mid-task, then (on a later attempt) runs step B. Each step writes a log row, so re-execution would be visible.
What happened, with timestamps from the database:
- 15:32:16.224 — step A checkpoints, then the worker dies.
- The task lease (5 minutes by default) expires; Postgres re-emits the task on its next tick.
- 15:37:21.557 — a fresh invocation resumes, skips A (its result is replayed from the checkpoint — the log shows exactly one "A" row), runs B, resolves the workflow.
Nothing ran twice. Recovery from a hard mid-task death is bounded by the task lease.
Being precise about the guarantee: steps are checkpointed exactly once — a completed step never re-executes on replay. A step interrupted after its side effect but before its checkpoint will re-run, so side effects are at-least-once, same as every durable execution system. Make your side effects idempotent or make them the last thing a step does. Two more precision notes. The shim doesn't heartbeat, so a step that runs longer than the five-minute lease gets a concurrent duplicate invocation, and both sides' side effects execute before the version fence picks a winner — keep steps shorter than the lease, or split them. And result-ready notifications are at-most-once: a lost one costs up to 60 s of latency while the SDK re-registers its listener (task execution delivery itself is retried), not correctness.
A durable AI agent, no extra infrastructure
The repo's example/research is a research agent built on the Anthropic SDK. The orchestration and state live entirely inside the Supabase project; the only external dependency is the Anthropic API.
plan (one LLM call, strict tool schema)
→ fan out N searches, each on its own Edge Function invocation
→ synthesize a cited report (one LLM call)
While the searches run, the parent isn't polling or holding a connection — it's a suspended row in Postgres waiting on its children. Our run planned 7 searches, executed them in parallel, and returned a 1,051-word cited report in 190 seconds.
Three of the seven searches hit their per-call timeout. The run didn't strand: the example treats a failed search as skippable, so the agent synthesized from the four that landed. That's the durable-agent argument in one sentence — failure handling becomes application logic instead of a babysitting process. Long-running agents survive redeploys and Edge Function wall-clock limits via checkpointing: a killed invocation retries from its last checkpoint instead of the start of the workflow, so keep any single call inside the platform's limit and fan out rather than making one long call. A multi-hour agent costs compute only in the moments it's actually thinking.
There's also a fully worked example repo that goes one step further: a durable agent loop (think → tool → observe) with a read-only SQL tool and a human-in-the-loop pause. When the agent calls ask_human, the workflow suspends as one Postgres row at zero compute. It resumes the moment someone answers with a single SQL call — whether that's thirty seconds later or the next day, up to the workflow's deadline. Every LLM turn and tool run is a checkpoint, so the agent survives crashes and redeploys mid-conversation. Code and walkthrough: https://github.com/resonatehq-examples/example-durable-agent-supabase-ts
Try it
- Server + examples: https://github.com/resonatehq/resonate-pg (Apache-2.0; the operational core is implemented — the protocol's search and list calls aren't yet, see the README)
- Durable agent with human-in-the-loop, fully worked: https://github.com/resonatehq-examples/example-durable-agent-supabase-ts
- SDK shim: https://jsr.io/@resonatehq/supabase
- How durable execution works: https://docs.resonatehq.io/?utm_source=devto&utm_medium=social&utm_campaign=resonate-on-supabase-2026-07
- Questions: https://resonatehq.io/discord
The countdown example goes from an empty Supabase project to a running durable workflow in about five minutes.
Top comments (0)