Skip to main content
ASoc
Tutorial

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.

The ASoc Team9 min read

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:

ApproachWhat it isWhere it breaks
Supabase database webhook → Webhooks by ZapierA Postgres trigger fires an HTTP POST at a Zap's catch hookNo 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 rowEvery 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 nodeAdds 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 verificationManual — you'd hand-roll HMAC checking inside a Zap's Code stepreq.text() against the raw body, built into the route
Multi-row writeSeparate steps, separate HTTP round tripsOne security definer function, one transaction
Retry safetyDepends on each step's own idempotency, if anyon conflict ... do nothing in the function itself
Where the logic livesSplit across a third-party UI you don't version-controlroute.ts + a migration file, both in this repo's git history
Cost at this scaleA paid Zapier tier past a low task ceilingFree — 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

SymptomCauseFix
Zap fires twice for one orderZapier's own delivery retries are at-least-once, not exactly-onceBuild idempotency into the receiving side (on conflict do nothing), never assume the trigger fires once
HMAC check fails on some payloadsBody was parsed to JSON before verifying, changing byte-for-byte contentVerify against the raw request body, before any JSON.parse
Zap silently stops runningZapier disables Zaps after repeated step failures with no alert to your appA 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 productionThe Zap's insert step has no on conflict equivalent for a retried triggerPush the uniqueness constraint into the database function, not the automation step
Supabase Zapier integration is missing from the app directoryIt genuinely isn't there — Supabase is listed "upcoming" on Zapier's own siteUse 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.

Keep reading

Tutorial8 min read

Tailwind Aspect Ratio: 22 Uses, Zero of Them aspect-video

Why this codebase writes aspect-[16/9] instead, and how the card's ratio has to agree with the img's width, height and the generated file's width.

Read more
Tutorial9 min read

Tailwind Button: 15 Utilities, 3 Variants, and Zero Buttons

The component called Button renders a link all 22 times it is used, while 44 real button elements sit elsewhere. The full class list, and the disabled state it was missing.

Read more