Database Webhooks in the Next.js Supabase Starter Kit

Handle database change events with webhooks to send notifications, sync external services, and trigger custom logic when data changes.

Database webhooks let you execute custom code when rows are inserted, updated, or deleted in your Supabase tables. Makerkit provides a typed webhook handler at @kit/database-webhooks that processes these events in a Next.js API route.

Database Webhooks Setup

Configure and handle database change events

How Database Webhooks Work

Supabase database webhooks fire HTTP requests to your application when specified database events occur. The flow is:

  1. A row is inserted, updated, or deleted in a table
  2. Supabase sends a POST request to your webhook endpoint
  3. Your handler processes the event and executes custom logic
  4. The handler returns a success response

Makerkit includes built-in handlers for:

  • User deletion: Cleans up related subscriptions and data
  • User signup: Sends welcome emails
  • Invitation creation: Sends invitation emails

You can extend this with your own handlers.

Adding Custom Webhook Handlers

The webhook endpoint is at apps/web/app/api/db/webhook/route.ts. Add your handlers to the handleEvent callback:

apps/web/app/api/db/webhook/route.ts

import { getDatabaseWebhookHandlerService } from '@kit/database-webhooks';
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async ({ request }) => {
const service = getDatabaseWebhookHandlerService();
try {
const signature = request.headers.get('X-Supabase-Event-Signature');
if (!signature) {
return new Response('Missing signature', { status: 400 });
}
const body = await request.clone().json();
await service.handleWebhook({
body,
signature,
async handleEvent(change) {
// Handle new project creation
if (change.type === 'INSERT' && change.table === 'projects') {
await notifyTeamOfNewProject(change.record);
}
// Handle subscription cancellation
if (change.type === 'UPDATE' && change.table === 'subscriptions') {
if (change.record.status === 'canceled') {
await sendCancellationSurvey(change.record);
}
}
// Handle user deletion
if (change.type === 'DELETE' && change.table === 'accounts') {
await cleanupExternalServices(change.old_record);
}
},
});
return new Response(null, { status: 200 });
} catch (error) {
console.error('Webhook error:', error);
return new Response(null, { status: 500 });
}
},
{ auth: false },
);

RecordChange Type

The change object is typed to your database schema:

import type { Database } from '@kit/supabase/database';
type Tables = Database['public']['Tables'];
type TableChangeType = 'INSERT' | 'UPDATE' | 'DELETE';
interface RecordChange<
Table extends keyof Tables,
Row = Tables[Table]['Row'],
> {
type: TableChangeType;
table: Table;
record: Row; // Current row data (null for DELETE)
schema: 'public';
old_record: Row | null; // Previous row data (null for INSERT)
}

Type-Safe Handlers

Cast to specific table types for better type safety:

import type { RecordChange } from '@kit/database-webhooks';
type ProjectChange = RecordChange<'projects'>;
type SubscriptionChange = RecordChange<'subscriptions'>;
async function handleEvent(change: RecordChange<keyof Tables>) {
if (change.table === 'projects') {
const projectChange = change as ProjectChange;
// projectChange.record is now typed to the projects table
console.log(projectChange.record.name);
}
}

Async Handlers

For long-running operations, consider using background jobs:

async handleEvent(change) {
if (change.type === 'INSERT' && change.table === 'orders') {
// Queue for background processing instead of blocking
await queueOrderProcessing(change.record.id);
}
}

Configuring Webhook Triggers

Webhooks are configured in Supabase. You can set them up via SQL or the Dashboard.

SQL Configuration

Add a trigger in your schema file at apps/web/supabase/schemas/:

apps/web/supabase/schemas/webhooks.sql

-- Create the webhook trigger for the projects table
create trigger projects_webhook
after insert or update or delete on public.projects
for each row execute function supabase_functions.http_request(
'https://your-app.com/api/db/webhook',
'POST',
'{"Content-Type":"application/json"}',
'{}',
'5000'
);

Dashboard Configuration

  1. Open your Supabase project dashboard
  2. Navigate to Database > Webhooks
  3. Click Create a new hook
  4. Configure:
    • Name: projects_webhook
    • Table: projects
    • Events: INSERT, UPDATE, DELETE
    • Type: HTTP Request
    • URL: https://your-app.com/api/db/webhook
    • Method: POST

Webhook Security

Supabase automatically signs webhook payloads using the X-Supabase-Event-Signature header. The @kit/database-webhooks package verifies this signature against your SUPABASE_DB_WEBHOOK_SECRET environment variable.

Configure the webhook secret:

.env.local

SUPABASE_DB_WEBHOOK_SECRET=your-webhook-secret

Set the same secret in your Supabase webhook configuration. The handler validates signatures automatically, rejecting requests with missing or invalid signatures.

Testing Webhooks Locally

Local Development Setup

When running Supabase locally, webhooks need to reach your Next.js server:

  1. Start your development server on a known port:
    pnpm run dev
  2. Configure the webhook URL in your local Supabase to point to http://host.docker.internal:3000/api/db/webhook (Docker) or http://localhost:3000/api/db/webhook.

Manual Testing

Test your webhook handler by sending a mock request:

curl -X POST http://localhost:3000/api/db/webhook \
-H "Content-Type: application/json" \
-H "x-webhook-secret: your-secret-key" \
-d '{
"type": "INSERT",
"table": "projects",
"schema": "public",
"record": {
"id": "test-id",
"name": "Test Project",
"account_id": "account-id"
},
"old_record": null
}'

Expected response: 200 OK

Debugging Tips

Webhook not firing: Check that the trigger exists in Supabase and the URL is correct.

Handler not executing: Add logging to trace the event flow:

async handleEvent(change) {
console.log('Received webhook:', {
type: change.type,
table: change.table,
recordId: change.record?.id,
});
}

Timeout errors: Move long operations to background jobs. Webhooks should respond quickly.

Common Use Cases

Use CaseTriggerAction
Welcome emailINSERT on usersSend onboarding email
Invitation emailINSERT on invitationsSend invite link
Subscription changeUPDATE on subscriptionsSync with CRM
User deletionDELETE on accountsClean up external services
Audit loggingINSERT/UPDATE/DELETEWrite to audit table
Search indexingINSERT/UPDATEUpdate search index