Files
myeasycms-v2/docs/development/database-webhooks.mdoc
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
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
2026-03-24 13:40:38 +08:00

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