Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
264 lines
7.9 KiB
Plaintext
264 lines
7.9 KiB
Plaintext
---
|
|
status: "published"
|
|
label: "Database Webhooks"
|
|
order: 6
|
|
title: "Database Webhooks in the Next.js Supabase Starter Kit"
|
|
description: "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.
|
|
|
|
{% sequence title="Database Webhooks Setup" description="Configure and handle database change events" %}
|
|
|
|
[Understand the webhook system](#how-database-webhooks-work)
|
|
|
|
[Add custom handlers](#adding-custom-webhook-handlers)
|
|
|
|
[Configure webhook triggers](#configuring-webhook-triggers)
|
|
|
|
[Test webhooks locally](#testing-webhooks-locally)
|
|
|
|
{% /sequence %}
|
|
|
|
## 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:
|
|
|
|
```tsx {% title="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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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/`:
|
|
|
|
```sql {% title="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:
|
|
|
|
```bash {% title=".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:
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3000/api/db/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Supabase-Event-Signature: 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:
|
|
|
|
```tsx
|
|
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 Case | Trigger | Action |
|
|
|----------|---------|--------|
|
|
| Welcome email | INSERT on `users` | Send onboarding email |
|
|
| Invitation email | INSERT on `invitations` | Send invite link |
|
|
| Subscription change | UPDATE on `subscriptions` | Sync with CRM |
|
|
| User deletion | DELETE on `accounts` | Clean up external services |
|
|
| Audit logging | INSERT/UPDATE/DELETE | Write to audit table |
|
|
| Search indexing | INSERT/UPDATE | Update search index |
|
|
|
|
## Related Resources
|
|
|
|
- [Database Schema](/docs/next-supabase-turbo/development/database-schema) for extending your schema
|
|
- [Database Functions](/docs/next-supabase-turbo/development/database-functions) for built-in SQL functions
|
|
- [Email Configuration](/docs/next-supabase-turbo/emails/email-configuration) for sending emails from webhooks
|