resonate-supabase-deployments-typescript — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-supabase-deployments-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.
Build durable workflows on Supabase Edge Functions using Resonate, with start/probe endpoints and optional progress tracking in Supabase.
Related: resonate-server-postgres — the server these workers talk to. Use it for installing resonate-pg, the pg_cron timer dependency, invoke's 24-hour default timeout, grants, retention, and the known gaps.
flows/index.ts workflow module plus start/probe endpoint usage patterns.supabase/functions directory.@resonatehq/supabase shim.RESONATE_URL: Resonate Server URL (Supabase secret or env var).SUPABASE_URL: Supabase project URL (for DB access).SUPABASE_SERVICE_ROLE_KEY: Service key for server-side DB operations.supabase/functions/flows/index.ts with registered workflows and handler.start and probe invocation.The edge function initiates communication with the Resonate server, NOT vice versa.
When triggered by an HTTP request, the edge function wakes up and polls the Resonate server for state. The shim handles all HTTP communication internally - you just write normal Resonate code.
HTTP Request → Edge Function wakes up → Polls Resonate Server → Executes workflow step → ReturnsThe Resonate server does NOT call your edge function. Your edge function calls the server.
// Option 1: Let shim read from env vars (RECOMMENDED)
const resonate = new Resonate();
// Reads RESONATE_URL and RESONATE_TOKEN automatically
// Option 2: Explicit configuration
const resonate = new Resonate({
url: Deno.env.get("RESONATE_URL")!,
token: Deno.env.get("RESONATE_TOKEN")
});Note: The TypeScript types may show { verbose?, encryptor? } - this is because url/token are read from env vars by default. Trust the skill documentation over type hints.
// ✅ CORRECT - Direct call
Deno.serve((req) => resonate.handler(req));
// ❌ WRONG - Factory pattern (old/incorrect)
Deno.serve((req) => resonate.handler()(req));This is the simplest working pattern for a Supabase edge function with Resonate:
// supabase/functions/flows/index.ts
import { Resonate, Context } from "@resonatehq/supabase";
const resonate = new Resonate();
function* myWorkflow(ctx: Context, name: string): Generator<any, string, any> {
yield* ctx.run(() => console.log(`Hello, ${name}!`));
yield* ctx.sleep(1000);
return `Completed for ${name}`;
}
resonate.register("myWorkflow", myWorkflow);
Deno.serve((req) => resonate.handler(req));That's it. No complex wrappers, no HTTP routing, no state management. The shim handles everything.
Resonate and Context and provides resonate.handler(req).start and probe endpoints manage top-level workflow execution.context.id is the execution id (passed via start), useful for DB correlation.yield* calls on Context.Intent: keep entrypoints consistent with Resonate shim routing.
Ensure this structure exists:
supabase/
|-- config.toml
`-- functions/
|-- flows/
| |-- deno.json
| `-- index.ts
|-- probe/
| |-- deno.json
| `-- index.ts
`-- start/
|-- deno.json
`-- index.tsDo not modify start/ and probe/ if they are provided by your template; only edit flows/index.ts.
flows/index.tsIntent: register durable functions and expose the Resonate handler.
// supabase/functions/flows/index.ts
import { Resonate, type Context } from "@resonatehq/supabase";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
// Initialize Resonate - reads RESONATE_URL from env by default
// For token auth, pass token explicitly or set RESONATE_TOKEN env var
const resonate = new Resonate({
url: Deno.env.get("RESONATE_URL")!,
token: Deno.env.get("RESONATE_TOKEN") // JWT token if auth required
});
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
resonate.setDependency("supabase", supabase);
function* processOrder(ctx: Context, orderId: string) {
const supabase = ctx.getDependency("supabase");
// persist progress
yield* ctx.run(async () => {
await supabase.from("order_progress").upsert({
id: ctx.id,
order_id: orderId,
status: "started",
updated_at: new Date().toISOString(),
});
});
const order = yield* ctx.run(loadOrder, orderId);
yield* ctx.run(async () => {
await supabase.from("order_progress").update({
status: "loaded",
updated_at: new Date().toISOString(),
}).eq("id", ctx.id);
});
// The SDK generates the promise id — create it first, then store the id so
// the approving client can find it.
const approval = yield* ctx.promise<boolean>();
yield* ctx.run(async () => {
await supabase.from("orders")
.update({ approval_promise_id: approval.id })
.eq("id", ctx.id);
});
const ok = yield* approval;
if (!ok) throw new Error("rejected");
return { orderId, status: "approved" };
}
async function loadOrder(_: Context, orderId: string) {
return { id: orderId };
}
resonate.register("processOrder", processOrder);
Deno.serve((req) => resonate.handler(req));startIntent: start durable execution from your app or curl.
const response = await fetch(`${SUPABASE_URL}/functions/v1/start`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SUPABASE_ANON_KEY}`,
},
body: JSON.stringify({
uuid: `order/${orderId}`,
func: "processOrder",
args: [orderId],
}),
});
const { uuid } = await response.json();probeIntent: poll or gate UI with execution state.
const response = await fetch(`${SUPABASE_URL}/functions/v1/probe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SUPABASE_ANON_KEY}`,
},
body: JSON.stringify({ uuid: `order/${orderId}` }),
});
const { status, value } = await response.json();Intent: resume a blocked workflow.
await fetch(`${RESONATE_URL}/promises/${promiseId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: "RESOLVED", value: { data: "true" } }),
});There are two patterns for starting workflows in Supabase Edge Functions:
If your template provides start/ and probe/ functions, use them:
// From your frontend or another service
const response = await fetch(`${SUPABASE_URL}/functions/v1/start`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SUPABASE_ANON_KEY}`,
},
body: JSON.stringify({
uuid: `countdown-${crypto.randomUUID()}`,
func: "durableCountdown",
args: [name, durationMinutes, targetTime],
}),
});The start/ function internally calls resonate.run() for you.
If you need custom routing or the template doesn't fit your needs:
// supabase/functions/flows/index.ts
import { Resonate, type Context } from "@resonatehq/supabase";
const resonate = new Resonate({
url: Deno.env.get("RESONATE_URL")!,
token: Deno.env.get("RESONATE_TOKEN")
});
function* durableCountdown(ctx: Context, name: string, durationMinutes: number) {
let remaining = durationMinutes;
while (remaining > 0) {
yield* ctx.run(sendWebhook, { type: "tick", name, remaining });
yield* ctx.sleep(60 * 1000);
remaining--;
}
yield* ctx.run(sendWebhook, { type: "complete", name });
return { status: "completed", name };
}
resonate.register("durableCountdown", durableCountdown);
// Custom request handler
Deno.serve(async (req) => {
const url = new URL(req.url);
// Start a new workflow
if (req.method === "POST" && url.pathname.endsWith("/start")) {
const { name, durationMinutes } = await req.json();
const promiseId = `countdown-${crypto.randomUUID()}`;
// This starts AND executes the workflow
resonate.run(promiseId, durableCountdown, name, durationMinutes);
return new Response(JSON.stringify({ promiseId }), {
headers: { "Content-Type": "application/json" }
});
}
// List active workflows by querying Resonate server directly
if (req.method === "GET" && url.pathname.endsWith("/list")) {
const resonateUrl = Deno.env.get("RESONATE_URL")!;
const token = Deno.env.get("RESONATE_TOKEN");
const response = await fetch(
`${resonateUrl}/promises?id=countdown-*&state=pending&limit=50`,
{
headers: {
"Content-Type": "application/json",
...(token && { "Authorization": `Bearer ${token}` })
}
}
);
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" }
});
}
// Default: let Resonate shim handle workflow execution callbacks
return resonate.handler(req); // Direct call, NOT a factory
});Do NOT create promises via direct HTTP API to start workflows.
// ❌ WRONG - This creates a promise but doesn't execute workflow code
await fetch(`${RESONATE_URL}/promises`, {
method: "POST",
body: JSON.stringify({ id: "my-workflow", timeout: 86400000 })
});
// ✅ CORRECT - This creates the promise AND executes the workflow
resonate.run("my-workflow", myWorkflowFunction, arg1, arg2);The HTTP API (POST /promises) is for creating standalone promises that will be resolved externally (like human-in-the-loop). To actually RUN workflow code, you must use resonate.run(), resonate.rpc(), or the start/ template endpoint.
function* reminder(ctx: Context, userId: string) {
yield* ctx.run(sendEmail, userId);
yield* ctx.sleep(24 * 60 * 60 * 1000);
yield* ctx.run(sendFollowUp, userId);
}function* validate(ctx: Context, orderId: string) {
const a = yield* ctx.beginRun(checkInventory, orderId);
const b = yield* ctx.beginRun(checkFraud, orderId);
return { inventory: yield* a, fraud: yield* b };
}context.id for DB correlationfunction* track(ctx: Context, userId: string) {
const supabase = ctx.getDependency("supabase");
yield* ctx.run(async () => {
await supabase.from("execution_progress").upsert({
id: ctx.id,
status: "started",
user_id: userId,
});
});
}1) Start a workflow and receive a UUID:
curl -X POST "$SUPABASE_URL/functions/v1/start" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-d '{"uuid":"order/123","func":"processOrder","args":["123"]}'Expected: JSON with uuid equal to order/123.
2) Probe the workflow:
curl -X POST "$SUPABASE_URL/functions/v1/probe" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-d '{"uuid":"order/123"}'Expected: status is pending, resolved, or rejected.
3) Confirm progress record exists (if using DB tracking):
select * from order_progress where id = 'order/123';Expected: a row with status started or later.
/functions/v1/start -> missing or wrong SUPABASE_ANON_KEY.start/probe -> functions not deployed or wrong function name.RESONATE_TOKEN not set or invalid JWT.promises endpoint.// In your edge function
const resonate = new Resonate({
url: Deno.env.get("RESONATE_URL")!,
token: Deno.env.get("RESONATE_TOKEN") // Just "token", not "auth: { ... }"
});
// For direct HTTP calls to Resonate server
const headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${Deno.env.get("RESONATE_TOKEN")}`
};ctx.run.async durable functions instead of function*.Date.now() or Math.random() inside durable functions.ctx.run.beginRun and immediate yield* in ways that defeat concurrency.ctx.promise() with a public webhook.resonate-http-service-design for HTTP gateway patterns.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.