# Resonate Supabase Deployments Typescript

> Build Resonate workflows on Supabase Edge Functions (TypeScript/Deno) using the Supabase shim, start/probe endpoints, and optional DB progress tracking.

- Skill: `resonatehq/resonate-supabase-deployments-typescript` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-supabase-deployments-typescript`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-supabase-deployments-typescript/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-supabase-deployments-typescript

---


# Resonate Supabase TypeScript

## Overview

Build durable workflows on Supabase Edge Functions using Resonate, with start/probe endpoints and optional progress tracking in Supabase.

Related: [`resonate-server-postgres`](../resonate-server-postgres/SKILL.md) — 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.

## Summary

- Build durable workflows on Supabase Edge Functions using the Resonate Supabase shim.
- Use when you need long-running, reliable, stepwise workflows triggered by HTTP or external events.
- Outputs a `flows/index.ts` workflow module plus start/probe endpoint usage patterns.

## Preconditions / Assumptions

- Supabase project with `supabase/functions` directory.
- Supabase Edge Functions runtime (Deno) is available.
- Resonate Server URL is known and reachable.
- Assumes a recent stable Resonate Server and `@resonatehq/supabase` shim.

## Inputs

- `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.
- Workflow name (function name to register).
- Execution ID scheme (uuid or domain id).

## Outputs

- `supabase/functions/flows/index.ts` with registered workflows and handler.
- HTTP endpoints for `start` and `probe` invocation.
- Optional DB table for progress tracking.

## CRITICAL: Architecture & Communication Direction

**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 → Returns
```

**The Resonate server does NOT call your edge function.** Your edge function calls the server.

## CRITICAL: Constructor & Handler Patterns

### Constructor - Reads from Environment Variables

```ts
// 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.

### Handler - Direct Call, NOT a Factory

```ts
// ✅ CORRECT - Direct call
Deno.serve((req) => resonate.handler(req));

// ❌ WRONG - Factory pattern (old/incorrect)
Deno.serve((req) => resonate.handler()(req));
```

## Minimal Complete Example

This is the simplest working pattern for a Supabase edge function with Resonate:

```ts
// 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.

## Core concepts (minimal)

- Supabase Edge Functions are short-lived; Resonate makes them durable via checkpoints.
- The Resonate Supabase shim exposes `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.
- Durable steps are all `yield*` calls on `Context`.

## Procedure

### 1) Confirm the Supabase function layout

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.ts
```

Do not modify `start/` and `probe/` if they are provided by your template; only edit `flows/index.ts`.

### 2) Implement workflows in `flows/index.ts`

Intent: register durable functions and expose the Resonate handler.

```ts
// 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));
```

### 3) Trigger a workflow via `start`

Intent: start durable execution from your app or curl.

```ts
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();
```

### 4) Check status via `probe`

Intent: poll or gate UI with execution state.

```ts
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();
```

### 5) Resolve external promises (HITL or webhook)

Intent: resume a blocked workflow.

```ts
await fetch(`${RESONATE_URL}/promises/${promiseId}`, {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ state: "RESOLVED", value: { data: "true" } }),
});
```

## Starting Workflows

There are two patterns for starting workflows in Supabase Edge Functions:

### Pattern A: Use the start/probe template (Recommended)

If your template provides `start/` and `probe/` functions, use them:

```ts
// 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.

### Pattern B: Custom Deno.serve() with resonate.run()

If you need custom routing or the template doesn't fit your needs:

```ts
// 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
});
```

### Key Insight: resonate.run() vs HTTP API

**Do NOT create promises via direct HTTP API to start workflows.**

```ts
// ❌ 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.

## Code patterns

### Durable sleep

```ts
function* reminder(ctx: Context, userId: string) {
  yield* ctx.run(sendEmail, userId);
  yield* ctx.sleep(24 * 60 * 60 * 1000);
  yield* ctx.run(sendFollowUp, userId);
}
```

### Structured concurrency (fork-join)

```ts
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 };
}
```

### Use `context.id` for DB correlation

```ts
function* 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,
    });
  });
}
```

## Verification

1) Start a workflow and receive a UUID:

```bash
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:

```bash
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):

```sql
select * from order_progress where id = 'order/123';
```

Expected: a row with status `started` or later.

## Troubleshooting

### Supabase Issues
- 401/403 calling `/functions/v1/start` -> missing or wrong `SUPABASE_ANON_KEY`.
- 404 for `start`/`probe` -> functions not deployed or wrong function name.

### Resonate Server Issues
- 401 from Resonate server -> `RESONATE_TOKEN` not set or invalid JWT.
- 401 on workflow execution -> token may be expired or server public key mismatch.
- Workflow never resumes -> promise not resolved; check `promises` endpoint.

### Auth Token Setup
```ts
// 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")}`
};
```

### Common Errors
- Duplicate side effects -> side effect not wrapped in `ctx.run`.
- Cached result returned -> execution id reused; use a new uuid.
- "Promise not found" -> workflow ID doesn't match or prefix is wrong.

## Pitfalls / Anti-patterns

- Using `async` durable functions instead of `function*`.
- Calling `Date.now()` or `Math.random()` inside durable functions.
- Returning non-serializable objects.
- Performing side effects outside `ctx.run`.
- Interleaving `beginRun` and immediate `yield*` in ways that defeat concurrency.

## Extensions (optional)

- Add Supabase Realtime to stream progress updates from a table.
- Add human-in-the-loop by pairing `ctx.promise()` with a public webhook.
- See `resonate-http-service-design` for HTTP gateway patterns.

