Supabase + Zapier: Why This Storefront's Webhooks Skip It
Supabase has no native Zapier integration. Here's the signed webhook route and one atomic Postgres RPC this storefront uses instead, and why a Zap chain can't match it.
Supabase has no native Zapier integration — the workaround is "Webhooks by Zapier" polling or receiving HTTP calls against Supabase's REST API. This storefront never needed that workaround: its one real Supabase-triggered automation, turning a paid order into download access, is a signed webhook route calling a single atomic Postgres function, not a multi-step Zap.
The short answer
No, Supabase and Zapier do not connect directly — Zapier's own integrations page lists Supabase as "upcoming," and the community's real answer is wiring Supabase's database webhooks or HTTP API to "Webhooks by Zapier" as a manual bridge. For anything that has to be correct — an order becoming an entitlement, a refund revoking access — that bridge is the wrong tool: it gives you an HTTP call with no ordering, retry, or transaction guarantee, chained to a third step that runs it. This codebase's own version of "when X happens in Supabase, do Y" skips the bridge and does the two things a Zap can't: verify the request came from the real source, and write every downstream row in one transaction.
What "Supabase + Zapier" actually means today
Confirmed straight from Supabase's own partner page and Zapier's community docs: there is no first-party connector. Three options exist for gluing the two together, in ascending order of how much they resemble automation-platform marketing rather than working code:
| Approach | What it is | Where it breaks |
|---|---|---|
| Supabase database webhook → Webhooks by Zapier | A Postgres trigger fires an HTTP POST at a Zap's catch hook | No delivery guarantee if the Zap endpoint is briefly down; no way to verify the payload's origin without hand-rolling a shared secret |
| Zap → Supabase HTTP API (PostgREST) | A Zap step calls Supabase's auto-generated REST API to read/write a row | Every write goes through PostgREST's request-response cycle per step — a five-step Zap is five round trips, not one transaction |
| Third-party connector (Latenode, n8n) | Another automation platform's own Supabase node | Adds an entire second vendor to the dependency chain for a job Postgres can do at the database layer |
None of the three gives you what a signed webhook handler backed by a database function gives you for free: one HTTP round trip, one transaction, and a verifiable sender.
The real trigger this codebase actually has
The one event that has to reliably turn into database rows here is LemonSqueezy's order_created webhook — a purchase becoming an entitlement. src/app/api/webhooks/lemonsqueezy/route.ts is the entire handler, and its first move is deliberate:
export async function POST(req: Request) {
const raw = await req.text();
const signature = req.headers.get("X-Signature");
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
const storeId = process.env.LEMONSQUEEZY_STORE_ID;
if (!secret || !storeId) {
console.error(
"lemonsqueezy webhook: LEMONSQUEEZY_WEBHOOK_SECRET/LEMONSQUEEZY_STORE_ID not configured",
);
return new Response("internal error", { status: 500 });
}
const db = createSupabaseWebhookDb(createAdminClient());
const result = await processWebhook(raw, signature, {
db,
secret,
storeId,
isProduction: process.env.NODE_ENV === "production",
// ...
});
return new Response(result.body, { status: result.status });
}
req.text() — never req.json() first. The HMAC signature LemonSqueezy sends has to be checked against the exact bytes it signed; re-serializing a parsed object can differ byte-for-byte from the original even when the data is identical, which would silently break signature verification on some payloads and not others. A Zap's catch-hook step gives you a parsed JSON body and nothing else — there is no way to recover the raw bytes to verify a signature after the platform has already deserialized it for you. This is the first thing "just Zap it" loses: you can't verify who sent the request once the platform has helpfully parsed the body for you.
The write itself is one atomic function, not a chain of steps
Once the signature checks out, turning the order into download access is a single call into Postgres, not an app-side sequence of "insert order, then insert slots, then check for a race." supabase/migrations/0003_commerce_rpcs.sql defines it:
create or replace function public.create_order_with_slots(
p_ls_order_id text, p_ls_customer_id text, p_email text, p_tier text,
p_total_cents int, p_currency text, p_user_id uuid, p_claim_email text,
p_raw jsonb, p_slot_kinds text[]
) returns table(order_id uuid, created boolean)
language plpgsql security definer set search_path = public, pg_temp as $$
declare v_order_id uuid; v_kind text;
begin
insert into public.orders (ls_order_id, ls_customer_id, email, tier, total_cents, currency, status, user_id, raw)
values (p_ls_order_id, p_ls_customer_id, p_email::extensions.citext, p_tier, p_total_cents, p_currency, 'paid', p_user_id, p_raw)
on conflict (ls_order_id) do nothing returning id into v_order_id;
if v_order_id is null then
select id into v_order_id from public.orders where ls_order_id = p_ls_order_id;
return query select v_order_id, false; return;
end if;
foreach v_kind in array p_slot_kinds loop
insert into public.entitlement_slots (order_id, user_id, claim_email, kind, status)
values (v_order_id, p_user_id, p_claim_email::extensions.citext, v_kind, 'active');
end loop;
return query select v_order_id, true;
end; $$;
on conflict (ls_order_id) do nothing is the whole idempotency story: LemonSqueezy retries webhooks it doesn't get a 200 for, so this function has to be safe to call twice with the same order. Run it again with the same ls_order_id and it reports created: false instead of writing duplicate entitlement slots. A Zap chain that inserts an order in step 2 and inserts slots in step 3 has no equivalent — a retried delivery either double-inserts the slots or, if a naive uniqueness check is bolted onto step 2 only, silently drops the entitlement write while step 3 still fires. The function is also security definer with execute revoked from anon/authenticated (R-3 in this repo's security rules): nothing reachable from the browser, or from a leaked Zapier webhook URL, can call it directly.
Automation platform vs. a signed webhook + one RPC
| Zapier (database webhook or HTTP API) | This codebase's webhook + RPC | |
|---|---|---|
| Sender verification | Manual — you'd hand-roll HMAC checking inside a Zap's Code step | req.text() against the raw body, built into the route |
| Multi-row write | Separate steps, separate HTTP round trips | One security definer function, one transaction |
| Retry safety | Depends on each step's own idempotency, if any | on conflict ... do nothing in the function itself |
| Where the logic lives | Split across a third-party UI you don't version-control | route.ts + a migration file, both in this repo's git history |
| Cost at this scale | A paid Zapier tier past a low task ceiling | Free — Postgres functions and Route Handlers, no per-task billing |
None of this is an argument that Zapier is bad — it is the right tool for connecting a CRM to a spreadsheet, or triggering a Slack message from a form submission, where an occasional double-fire costs nothing. It is the wrong tool for the one place this storefront actually needs "when Supabase changes, make something else happen": turning money into access, where a double-write or an unverified sender is a security bug, not an inconvenience.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Zap fires twice for one order | Zapier's own delivery retries are at-least-once, not exactly-once | Build idempotency into the receiving side (on conflict do nothing), never assume the trigger fires once |
| HMAC check fails on some payloads | Body was parsed to JSON before verifying, changing byte-for-byte content | Verify against the raw request body, before any JSON.parse |
| Zap silently stops running | Zapier disables Zaps after repeated step failures with no alert to your app | A webhook route you own returns real HTTP status codes your own logs and alerting can see |
| "It worked in testing" but rows are missing in production | The Zap's insert step has no on conflict equivalent for a retried trigger | Push the uniqueness constraint into the database function, not the automation step |
| Supabase Zapier integration is missing from the app directory | It genuinely isn't there — Supabase is listed "upcoming" on Zapier's own site | Use Supabase database webhooks to a URL you control, or skip the automation platform for anything transactional |
Frequently asked questions
Does Supabase have an official Zapier integration? No. Supabase is listed as an "upcoming" app on Zapier's own integrations page as of this writing. The working paths are Supabase's database webhooks calling a Zapier catch hook, or a Zap step calling Supabase's PostgREST HTTP API — both are you wiring generic HTTP, not a native connector.
Is it fine to use Zapier with Supabase for anything? Yes, for automations where an occasional retry or a few seconds of delay cost nothing — syncing a new signup into a mailing list, posting a Slack message when a row is inserted. It's the wrong choice for anything that has to be exactly-once and verifiable, like turning a payment into paid access.
What replaces Zapier for a transactional Supabase event?
A webhook route you own (verifying the sender against the raw request body) calling a security definer Postgres function that does the multi-table write atomically, with a conflict clause that makes retries safe. That's the whole pattern this storefront runs on for its own paid-order-to-entitlement flow.
Why not just add a uniqueness constraint and call it from separate Zap steps? A uniqueness constraint on one table doesn't protect the tables after it in the chain. If step 2 (insert order) is protected but step 3 (insert entitlement slots) isn't, a retried trigger produces an order that correctly didn't duplicate and slots that did. The fix has to live in one transaction, which is exactly what a Zap's separate steps can't give you.
Where to take this next
The webhook route above is the same one LemonSqueezy vs Stripe for digital products measures for signature-verification overhead, and LemonSqueezy vs Polar inventories the vendor-specific code a checkout-provider switch would have to rewrite — this webhook route and RPC pair are exactly what moves. For the migration file's own SECURITY DEFINER conventions in full, see Supabase migrations.
Templates in this post
ASoc Pip, ASoc Press and ASoc Quest ship as plain Next.js + Tailwind landing pages — no Supabase, no webhooks, nothing to wire up before the page is live.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
