CloudEvents
Mutations as CloudEvents 1.0, for queues, buses and webhooks.
better-supabase/events turns repository mutations into
CloudEvents and sends them to an EventSink.
import { forwardMutations, httpSink } from "better-supabase/events";
const stop = forwardMutations(
betterSupabase,
httpSink("https://events.example.com/ingest"),
{
source: "https://crm.example.com",
filter: (notice) => notice.table !== "auditLog",
},
);Each mutated row becomes one event:
{
"specversion": "1.0",
"id": "5f0c…",
"source": "https://crm.example.com",
"type": "dev.better-supabase.row.created",
"subject": "customers/5f0c…",
"time": "2026-01-01T00:00:00.000Z",
"datacontenttype": "application/json",
"data": {
"table": "customers",
"row": { "id": "5f0c…", "name": "Acme" },
"actorId": "user-1"
},
"partitionkey": "org-1"
}The types are row.created, row.updated, row.upserted, row.deleted and
row.softdeleted. Use typePrefix: 'com.acme.crm' to use your own namespace.
subject is the primary key, partitionkey the tenant (also the one
tenant() resolved from the claims) and data.actorId the acting user. The
actor is in data, not in a context attribute, because intermediaries read
context attributes and they must not carry personal data. Writes
that return no rows send one event per known primary key, with the key as
data.row.
Sinks
EventSink has one method, send(events). Implement it for SQS, Pub/Sub,
Inngest or an outbox table. Sink failures go to onError and never fail
the mutation. Events are forwarded after the write, so use the SQL modules'
outbox when every event must arrive.
httpSink(url, { mode }) POSTs in the HTTP binding's batch (default),
structured or binary mode. On the receiving side, fromHttp(request)
reads all three modes, and isCloudEvent validates the required attributes.
toCloudEvents(notice, options) builds events without a sink.
Sends after the response
A sink's send runs after the write, so a serverless function can stop
before it finishes. forwardMutations tracks each send on
betterSupabase.events: events.pending says whether one is running and
events.settled() resolves when they all have, failed ones included. The
adapters wait for them for you. bs.route and bs.action in Next.js pass
them to after(). The edge handlers, the Hono middleware, oRPC's
fetchHandler and both MCP adapters pass them to the waitUntil option (see
Edge Functions); Hono also uses
c.executionCtx on Workers. Elsewhere, await
betterSupabase.events.settled() before the process exits.
Last updated on