Skip to content

Commit 681b1db

Browse files
author
Jeroen Peeters
committed
feat(plugins): add Clerk plugin to manage users with webhooks
chore: update pnpm-lock.yaml with new package resolutions
1 parent 303e04f commit 681b1db

File tree

11 files changed

+324
-77
lines changed

11 files changed

+324
-77
lines changed

dist/plugins.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { StripeSubscriptionPlugin } from '../plugins/stripe'
55
export { ChangeDataCapturePlugin } from '../plugins/cdc'
66
export { QueryLogPlugin } from '../plugins/query-log'
77
export { ResendPlugin } from '../plugins/resend'
8+
export { ClerkPlugin } from '../plugins/clerk'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"mysql2": "^3.11.4",
5959
"node-sql-parser": "^4.18.0",
6060
"pg": "^8.13.1",
61+
"svix": "^1.59.2",
6162
"tailwind-merge": "^2.6.0",
6263
"vite": "^5.4.11"
6364
},

plugins/clerk/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Clerk Plugin
2+
3+
The Clerk Plugin for Starbase provides a quick and simple way for applications to add Clerk user information to their database.
4+
5+
For more information on how to setup webhooks for your Clerk instance, please refer to their excellent [guide](https://clerk.com/docs/webhooks/sync-data).
6+
7+
## Usage
8+
9+
Add the ClerkPlugin plugin to your Starbase configuration:
10+
11+
```typescript
12+
import { ClerkPlugin } from './plugins/clerk'
13+
const plugins = [
14+
// ... other plugins
15+
new ClerkPlugin({
16+
clerkInstanceId: 'ins_**********',
17+
clerkSigningSecret: 'whsec_**********',
18+
}),
19+
] satisfies StarbasePlugin[]
20+
```
21+
22+
## Configuration Options
23+
24+
| Option | Type | Default | Description |
25+
| -------------------- | ------ | ------- | --------------------------------------------------------------------------------------- |
26+
| `clerkInstanceId` | string | `null` | Access your instance ID from (https://dashboard.clerk.com/last-active?path=settings) |
27+
| `clerkSigningSecret` | string | `null` | Access your signing secret from (https://dashboard.clerk.com/last-active?path=webhooks) |
28+
29+
## How To Use
30+
31+
### Webhook Setup
32+
33+
For our Starbase instance to receive webhook events when user information changes, we need to add our plugin endpoint to Clerk.
34+
35+
1. Visit the Webhooks page for your Clerk instance: https://dashboard.clerk.com/last-active?path=webhooks
36+
2. Add a new endpoint with the following settings:
37+
- URL: `https://<your-starbase-instance-url>/clerk/webhook`
38+
- Events: `User`
39+
3. Save by clicking "Create"

plugins/clerk/index.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Webhook } from 'svix'
2+
import { StarbaseApp, StarbaseContext } from '../../src/handler'
3+
import { StarbasePlugin } from '../../src/plugin'
4+
import { createResponse } from '../../src/utils'
5+
import CREATE_TABLE from './sql/create-table.sql'
6+
import UPSERT_USER from './sql/upsert-user.sql'
7+
import GET_USER_INFORMATION from './sql/get-user-information.sql'
8+
import DELETE_USER from './sql/delete-user.sql'
9+
10+
type ClerkEvent = {
11+
instance_id: string
12+
} & (
13+
| {
14+
type: 'user.created' | 'user.updated'
15+
data: {
16+
id: string
17+
first_name: string
18+
last_name: string
19+
email_addresses: Array<{
20+
id: string
21+
email_address: string
22+
}>
23+
primary_email_address_id: string
24+
}
25+
}
26+
| {
27+
type: 'user.deleted'
28+
data: { id: string }
29+
}
30+
)
31+
32+
const SQL_QUERIES = {
33+
CREATE_TABLE,
34+
UPSERT_USER,
35+
GET_USER_INFORMATION, // Currently not used, but can be turned into an endpoint
36+
DELETE_USER,
37+
}
38+
39+
export class ClerkPlugin extends StarbasePlugin {
40+
context?: StarbaseContext
41+
pathPrefix: string = '/clerk'
42+
clerkInstanceId?: string
43+
clerkSigningSecret: string
44+
45+
constructor(opts?: {
46+
clerkInstanceId?: string
47+
clerkSigningSecret: string
48+
}) {
49+
super('starbasedb:clerk', {
50+
// The `requiresAuth` is set to false to allow for the webhooks sent by Clerk to be accessible
51+
requiresAuth: false,
52+
})
53+
if (!opts?.clerkSigningSecret) {
54+
throw new Error('A signing secret is required for this plugin.')
55+
}
56+
this.clerkInstanceId = opts.clerkInstanceId
57+
this.clerkSigningSecret = opts.clerkSigningSecret
58+
}
59+
60+
override async register(app: StarbaseApp) {
61+
app.use(async (c, next) => {
62+
this.context = c
63+
const dataSource = c?.get('dataSource')
64+
65+
// Create user table if it doesn't exist
66+
await dataSource?.rpc.executeQuery({
67+
sql: SQL_QUERIES.CREATE_TABLE,
68+
params: [],
69+
})
70+
71+
await next()
72+
})
73+
74+
// Webhook to handle Clerk events
75+
app.post(`${this.pathPrefix}/webhook`, async (c) => {
76+
const wh = new Webhook(this.clerkSigningSecret)
77+
const svix_id = c.req.header('svix-id')
78+
const svix_signature = c.req.header('svix-signature')
79+
const svix_timestamp = c.req.header('svix-timestamp')
80+
81+
if (!svix_id || !svix_signature || !svix_timestamp) {
82+
return createResponse(
83+
undefined,
84+
'Missing required headers: svix-id, svix-signature, svix-timestamp',
85+
400
86+
)
87+
}
88+
89+
const body = await c.req.text()
90+
const dataSource = this.context?.get('dataSource')
91+
92+
try {
93+
const event = wh.verify(body, {
94+
'svix-id': svix_id,
95+
'svix-timestamp': svix_timestamp,
96+
'svix-signature': svix_signature,
97+
}) as ClerkEvent
98+
99+
if (this.clerkInstanceId && 'instance_id' in event && event.instance_id !== this.clerkInstanceId) {
100+
return createResponse(
101+
undefined,
102+
'Invalid instance ID',
103+
401
104+
)
105+
}
106+
107+
if (event.type === 'user.deleted') {
108+
const { id } = event.data
109+
110+
await dataSource?.rpc.executeQuery({
111+
sql: SQL_QUERIES.DELETE_USER,
112+
params: [id],
113+
})
114+
} else if (
115+
event.type === 'user.updated' ||
116+
event.type === 'user.created'
117+
) {
118+
const { id, first_name, last_name, email_addresses, primary_email_address_id } = event.data
119+
120+
const email = email_addresses.find(
121+
(email: any) => email.id === primary_email_address_id
122+
)?.email_address
123+
124+
await dataSource?.rpc.executeQuery({
125+
sql: SQL_QUERIES.UPSERT_USER,
126+
params: [id, email, first_name, last_name],
127+
})
128+
}
129+
130+
return createResponse({ success: true }, undefined, 200)
131+
} catch (error: any) {
132+
console.error('Webhook processing error:', error)
133+
return createResponse(
134+
undefined,
135+
`Webhook processing failed: ${error.message}`,
136+
400
137+
)
138+
}
139+
})
140+
}
141+
}

plugins/clerk/meta.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"version": "1.0.0",
3+
"resources": {
4+
"tables": {
5+
"user": [
6+
"user_id",
7+
"email",
8+
"first_name",
9+
"last_name",
10+
"created_at",
11+
"updated_at",
12+
"deleted_at"
13+
]
14+
},
15+
"secrets": {},
16+
"variables": {}
17+
},
18+
"dependencies": {
19+
"tables": {
20+
"*": ["user_id"]
21+
},
22+
"secrets": {},
23+
"variables": {}
24+
}
25+
}

plugins/clerk/sql/create-table.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CREATE TABLE IF NOT EXISTS user (
2+
user_id TEXT PRIMARY KEY,
3+
email TEXT NOT NULL,
4+
first_name TEXT,
5+
last_name TEXT,
6+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
7+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
8+
deleted_at DATETIME DEFAULT NULL
9+
)

plugins/clerk/sql/delete-user.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
UPDATE user
2+
SET deleted_at = CURRENT_TIMESTAMP
3+
WHERE user_id = ? AND deleted_at IS NULL
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SELECT email, first_name, last_name FROM user
2+
WHERE user_id = ? AND deleted_at IS NULL

plugins/clerk/sql/upsert-user.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
INSERT INTO user (user_id, email, first_name, last_name)
2+
VALUES (?, ?, ?, ?)
3+
ON CONFLICT(user_id) DO UPDATE SET
4+
email = excluded.email,
5+
first_name = excluded.first_name,
6+
last_name = excluded.last_name,
7+
updated_at = CURRENT_TIMESTAMP,
8+
deleted_at = NULL

0 commit comments

Comments
 (0)