Build a reliable webhook handler that keeps Paddle subscription state in sync with Supabase using Edge Functions, signature verification, and idempotent upserts.

Paddle Webhook Events Worth Handling

Paddle sends subscription.* events for created, updated, canceled, and paused states. Only subscribe to the minimal set your product needs; extra events increase surface area for failures and retries.

Map each event to a single upsert on a subscriptions table that includes paddle_subscription_id as the natural key. This mirrors the pattern in How to Implement Idempotent Webhooks with Postgres Unique Constraints.

Create the Supabase Edge Function Endpoint

Use a Supabase Edge Function to receive the POST. The function must read the raw body for signature verification before any JSON parsing occurs.

import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const signature = req.headers.get('Paddle-Signature');
  const body = await req.text();
  // verify signature here
  const event = JSON.parse(body);
  // process event
});

Verify Paddle Signatures and Enforce Idempotency

Paddle signs webhooks with an RSA public key you fetch from their API. Cache the key for 24 hours inside the function to avoid extra latency. Reject any request whose signature fails.

Store the Paddle event_id in a webhook_events table with a unique constraint. This prevents duplicate processing even if Paddle retries the same delivery multiple times.

Process Subscription Lifecycle Events

On subscription.created or subscription.updated, upsert the record with status, current_period_end, and plan_id. On canceled, set status to canceled and record the cancel date rather than deleting the row.

Always return 200 within 5 seconds. If your handler needs longer work, write the event to a queue table and acknowledge immediately.

Deploy, Configure Paddle, and Test Retries

Deploy the function with supabase functions deploy paddle-webhook. In the Paddle dashboard, add the function URL as a webhook endpoint and select only the subscription events you handle.

Use Paddle’s sandbox to send test events. Trigger a subscription update, then immediately cancel it to verify both paths update the same Supabase row without conflict.

Production Gotchas and Cost Controls

Edge Functions have a 60-second timeout; any synchronous Supabase call that hangs will drop the webhook. Add explicit timeouts and circuit breakers around the database client.

At scale, repeated failed deliveries from Paddle can generate duplicate load. The idempotency table plus a short retention policy on webhook_events keeps storage and query cost predictable.