@odvi/create-dtt-framework 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +16 -13
- package/dist/commands/create.js.map +1 -1
- package/package.json +3 -2
- package/template/.env.example +103 -0
- package/template/components.json +22 -0
- package/template/docs/framework/01-overview.md +289 -0
- package/template/docs/framework/02-techstack.md +503 -0
- package/template/docs/framework/api-layer.md +681 -0
- package/template/docs/framework/clerk-authentication.md +649 -0
- package/template/docs/framework/cli-installation.md +564 -0
- package/template/docs/framework/deployment/ci-cd.md +907 -0
- package/template/docs/framework/deployment/digitalocean.md +991 -0
- package/template/docs/framework/deployment/domain-setup.md +972 -0
- package/template/docs/framework/deployment/environment-variables.md +863 -0
- package/template/docs/framework/deployment/monitoring.md +927 -0
- package/template/docs/framework/deployment/production-checklist.md +649 -0
- package/template/docs/framework/deployment/vercel.md +791 -0
- package/template/docs/framework/environment-variables.md +658 -0
- package/template/docs/framework/health-check-system.md +582 -0
- package/template/docs/framework/implementation.md +559 -0
- package/template/docs/framework/snowflake-integration.md +591 -0
- package/template/docs/framework/state-management.md +615 -0
- package/template/docs/framework/supabase-integration.md +581 -0
- package/template/docs/framework/testing-guide.md +544 -0
- package/template/docs/framework/what-did-i-miss.md +526 -0
- package/template/drizzle.config.ts +12 -0
- package/template/next.config.js +21 -0
- package/template/postcss.config.js +5 -0
- package/template/prettier.config.js +4 -0
- package/template/public/favicon.ico +0 -0
- package/template/src/app/(auth)/layout.tsx +4 -0
- package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
- package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
- package/template/src/app/(dashboard)/health/page.tsx +16 -0
- package/template/src/app/(dashboard)/layout.tsx +17 -0
- package/template/src/app/api/[[...route]]/route.ts +11 -0
- package/template/src/app/api/debug-files/route.ts +33 -0
- package/template/src/app/api/webhooks/clerk/route.ts +112 -0
- package/template/src/app/layout.tsx +28 -0
- package/template/src/app/page.tsx +12 -0
- package/template/src/app/providers.tsx +20 -0
- package/template/src/components/layouts/navbar.tsx +14 -0
- package/template/src/components/shared/loading-spinner.tsx +6 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +62 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/scroll-area.tsx +58 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/config/__tests__/env.test.ts +166 -0
- package/template/src/config/__tests__/site.test.ts +46 -0
- package/template/src/config/env.ts +36 -0
- package/template/src/config/site.ts +10 -0
- package/template/src/env.js +44 -0
- package/template/src/features/__tests__/health-check-config.test.ts +142 -0
- package/template/src/features/__tests__/health-check-types.test.ts +201 -0
- package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
- package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
- package/template/src/features/documentation/index.tsx +92 -0
- package/template/src/features/documentation/utils/doc-loader.ts +177 -0
- package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
- package/template/src/features/health-check/config.ts +72 -0
- package/template/src/features/health-check/index.ts +4 -0
- package/template/src/features/health-check/stores/health-store.ts +14 -0
- package/template/src/features/health-check/types.ts +18 -0
- package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
- package/template/src/hooks/queries/use-health-checks.ts +16 -0
- package/template/src/hooks/utils/use-debounce.ts +20 -0
- package/template/src/lib/__tests__/utils.test.ts +52 -0
- package/template/src/lib/__tests__/validators.test.ts +114 -0
- package/template/src/lib/nextbank/client.ts +37 -0
- package/template/src/lib/snowflake/client.ts +53 -0
- package/template/src/lib/supabase/admin.ts +7 -0
- package/template/src/lib/supabase/client.ts +7 -0
- package/template/src/lib/supabase/server.ts +23 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/lib/validators.ts +9 -0
- package/template/src/middleware.ts +22 -0
- package/template/src/server/api/index.ts +22 -0
- package/template/src/server/api/middleware/auth.ts +19 -0
- package/template/src/server/api/middleware/logger.ts +4 -0
- package/template/src/server/api/routes/health/clerk.ts +214 -0
- package/template/src/server/api/routes/health/database.ts +117 -0
- package/template/src/server/api/routes/health/edge-functions.ts +75 -0
- package/template/src/server/api/routes/health/framework.ts +45 -0
- package/template/src/server/api/routes/health/index.ts +102 -0
- package/template/src/server/api/routes/health/nextbank.ts +67 -0
- package/template/src/server/api/routes/health/snowflake.ts +83 -0
- package/template/src/server/api/routes/health/storage.ts +163 -0
- package/template/src/server/api/routes/users.ts +95 -0
- package/template/src/server/db/index.ts +17 -0
- package/template/src/server/db/queries/users.ts +8 -0
- package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
- package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
- package/template/src/server/db/schema/health-checks.ts +11 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/users.ts +16 -0
- package/template/src/server/db/schema.ts +26 -0
- package/template/src/stores/__tests__/ui-store.test.ts +87 -0
- package/template/src/stores/ui-store.ts +14 -0
- package/template/src/styles/globals.css +129 -0
- package/template/src/test/mocks/clerk.ts +35 -0
- package/template/src/test/mocks/snowflake.ts +28 -0
- package/template/src/test/mocks/supabase.ts +37 -0
- package/template/src/test/setup.ts +69 -0
- package/template/src/test/utils/test-helpers.ts +158 -0
- package/template/src/types/index.ts +14 -0
- package/template/tsconfig.json +43 -0
- package/template/vitest.config.ts +44 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# DTT Framework - Clerk Authentication
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The DTT Framework uses [Clerk](https://clerk.com/) as the authentication and user management solution. Clerk provides a complete authentication system with built-in UI components, organization management, and webhook support for synchronizing user data with your local database.
|
|
6
|
+
|
|
7
|
+
### Why Clerk?
|
|
8
|
+
|
|
9
|
+
- **Complete Auth Solution**: Sign-in, sign-up, password reset, email verification, MFA
|
|
10
|
+
- **Built-in Components**: Pre-built, customizable auth UI components
|
|
11
|
+
- **Organization Support**: Multi-tenant architecture out of the box
|
|
12
|
+
- **Webhooks**: Real-time user data synchronization
|
|
13
|
+
- **Type Safety**: Full TypeScript support
|
|
14
|
+
- **Security**: SOC 2 Type II compliant, GDPR ready
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Clerk Integration Setup
|
|
19
|
+
|
|
20
|
+
### 1. Create a Clerk Account
|
|
21
|
+
|
|
22
|
+
1. Go to [clerk.com](https://clerk.com/) and sign up
|
|
23
|
+
2. Create a new application
|
|
24
|
+
3. Note down your API keys from the dashboard
|
|
25
|
+
|
|
26
|
+
### 2. Install Clerk Dependencies
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @clerk/nextjs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 3. Configure Environment Variables
|
|
33
|
+
|
|
34
|
+
Add the following to your [`.env`](../environment-variables.md) file:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Clerk
|
|
38
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
|
|
39
|
+
CLERK_SECRET_KEY=sk_test_xxx
|
|
40
|
+
CLERK_WEBHOOK_SECRET=whsec_xxx
|
|
41
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
42
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
43
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/health
|
|
44
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/health
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Where to find these keys:**
|
|
48
|
+
|
|
49
|
+
- **Publishable Key**: Clerk Dashboard → Your App → API Keys → Publishable Key
|
|
50
|
+
- **Secret Key**: Clerk Dashboard → Your App → API Keys → Secret Key
|
|
51
|
+
- **Webhook Secret**: Clerk Dashboard → Your App → Webhooks → Add Endpoint → Copy Secret
|
|
52
|
+
|
|
53
|
+
### 4. Configure Environment Validation
|
|
54
|
+
|
|
55
|
+
Update [`src/config/env.ts`](../../src/config/env.ts) to validate Clerk environment variables:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createEnv } from '@t3-oss/env-nextjs'
|
|
59
|
+
import { z } from 'zod'
|
|
60
|
+
|
|
61
|
+
export const env = createEnv({
|
|
62
|
+
server: {
|
|
63
|
+
// ... other variables
|
|
64
|
+
CLERK_SECRET_KEY: z.string().startsWith('sk_'),
|
|
65
|
+
CLERK_WEBHOOK_SECRET: z.string().optional(),
|
|
66
|
+
},
|
|
67
|
+
client: {
|
|
68
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
|
|
69
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL: z.string().default('/sign-in'),
|
|
70
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL: z.string().default('/sign-up'),
|
|
71
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL: z.string().default('/health'),
|
|
72
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL: z.string().default('/health'),
|
|
73
|
+
},
|
|
74
|
+
runtimeEnv: {
|
|
75
|
+
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
|
76
|
+
CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET,
|
|
77
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
|
78
|
+
NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL,
|
|
79
|
+
NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL,
|
|
80
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL,
|
|
81
|
+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## User Authentication Flow
|
|
89
|
+
|
|
90
|
+
### Authentication Flow Diagram
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
94
|
+
│ User │────▶│ Clerk UI │────▶│ Clerk API │────▶│ Session │
|
|
95
|
+
│ Browser │ │ Components │ │ Server │ │ Created │
|
|
96
|
+
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
97
|
+
│ │
|
|
98
|
+
│ ▼
|
|
99
|
+
│ ┌─────────────┐
|
|
100
|
+
│ │ Clerk │
|
|
101
|
+
│ │ Webhook │
|
|
102
|
+
│ └─────────────┘
|
|
103
|
+
│ │
|
|
104
|
+
▼ ▼
|
|
105
|
+
┌─────────────┐ ┌─────────────┐
|
|
106
|
+
│ Redirect │ │ Local DB │
|
|
107
|
+
│ to App │ │ Updated │
|
|
108
|
+
└─────────────┘ └─────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Sign-In Flow
|
|
112
|
+
|
|
113
|
+
1. User navigates to `/sign-in`
|
|
114
|
+
2. Clerk `<SignIn />` component renders
|
|
115
|
+
3. User enters credentials
|
|
116
|
+
4. Clerk validates credentials
|
|
117
|
+
5. Clerk creates session and stores tokens
|
|
118
|
+
6. User is redirected to `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL`
|
|
119
|
+
|
|
120
|
+
### Sign-Up Flow
|
|
121
|
+
|
|
122
|
+
1. User navigates to `/sign-up`
|
|
123
|
+
2. Clerk `<SignUp />` component renders
|
|
124
|
+
3. User enters registration details
|
|
125
|
+
4. Clerk validates and creates user account
|
|
126
|
+
5. Clerk triggers `user.created` webhook
|
|
127
|
+
6. Webhook handler creates user in local database
|
|
128
|
+
7. User is redirected to `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL`
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Organization Management
|
|
133
|
+
|
|
134
|
+
### Clerk Organizations
|
|
135
|
+
|
|
136
|
+
Clerk provides built-in organization management for multi-tenant applications. Organizations allow you to group users and manage permissions at the organization level.
|
|
137
|
+
|
|
138
|
+
### Organization Features
|
|
139
|
+
|
|
140
|
+
- **Multi-Tenancy**: Separate data per organization
|
|
141
|
+
- **Organization Roles**: Admin, Member, etc.
|
|
142
|
+
- **Organization Memberships**: Users can belong to multiple organizations
|
|
143
|
+
- **Organization Webhooks**: Sync organization membership changes
|
|
144
|
+
|
|
145
|
+
### Organization Schema
|
|
146
|
+
|
|
147
|
+
The local database schema includes organization tracking:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// src/server/db/schema/users.ts
|
|
151
|
+
export const users = pgTable('users', {
|
|
152
|
+
id: text('id').primaryKey(), // Clerk user ID
|
|
153
|
+
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
154
|
+
firstName: varchar('first_name', { length: 255 }),
|
|
155
|
+
lastName: varchar('last_name', { length: 255 }),
|
|
156
|
+
imageUrl: text('image_url'),
|
|
157
|
+
clerkOrgId: text('clerk_org_id'), // Clerk organization ID
|
|
158
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
159
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Accessing Organization Data
|
|
164
|
+
|
|
165
|
+
**Server-Side:**
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { getAuth } from '@clerk/nextjs/server'
|
|
169
|
+
|
|
170
|
+
export async function getServerData() {
|
|
171
|
+
const auth = getAuth()
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
userId: auth.userId,
|
|
175
|
+
orgId: auth.orgId,
|
|
176
|
+
orgRole: auth.orgRole,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Client-Side:**
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { useAuth } from '@clerk/nextjs'
|
|
185
|
+
|
|
186
|
+
export function MyComponent() {
|
|
187
|
+
const { orgId, orgRole } = useAuth()
|
|
188
|
+
|
|
189
|
+
return <div>Organization: {orgId}, Role: {orgRole}</div>
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Webhook Synchronization
|
|
196
|
+
|
|
197
|
+
### Webhook Setup
|
|
198
|
+
|
|
199
|
+
1. Go to Clerk Dashboard → Your App → Webhooks
|
|
200
|
+
2. Add a new endpoint: `https://your-domain.com/api/webhooks/clerk`
|
|
201
|
+
3. Select events to listen for:
|
|
202
|
+
- `user.created`
|
|
203
|
+
- `user.updated`
|
|
204
|
+
- `user.deleted`
|
|
205
|
+
- `organizationMembership.created`
|
|
206
|
+
- `organizationMembership.updated`
|
|
207
|
+
4. Copy the webhook secret to your `.env` file
|
|
208
|
+
|
|
209
|
+
### Webhook Handler
|
|
210
|
+
|
|
211
|
+
The webhook handler is located at [`src/app/api/webhooks/clerk/route.ts`](../../src/app/api/webhooks/clerk/route.ts):
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import type { WebhookEvent } from '@clerk/nextjs/server'
|
|
215
|
+
import { headers } from 'next/headers'
|
|
216
|
+
import { Webhook } from 'svix'
|
|
217
|
+
import { env } from '@/config/env'
|
|
218
|
+
import { db } from '@/server/db'
|
|
219
|
+
import { users } from '@/server/db/schema/users'
|
|
220
|
+
import { eq } from 'drizzle-orm'
|
|
221
|
+
|
|
222
|
+
export async function POST(req: Request) {
|
|
223
|
+
try {
|
|
224
|
+
// Get headers
|
|
225
|
+
const headerPayload = await headers()
|
|
226
|
+
const svix_id = headerPayload.get('svix-id')
|
|
227
|
+
const svix_timestamp = headerPayload.get('svix-timestamp')
|
|
228
|
+
const svix_signature = headerPayload.get('svix-signature')
|
|
229
|
+
|
|
230
|
+
// Verify headers
|
|
231
|
+
if (!svix_id || !svix_timestamp || !svix_signature) {
|
|
232
|
+
return new Response('Error: Missing Svix headers', { status: 400 })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get the body
|
|
236
|
+
const payload = await req.json()
|
|
237
|
+
const body = JSON.stringify(payload)
|
|
238
|
+
|
|
239
|
+
// Verify webhook signature
|
|
240
|
+
const wh = new Webhook(env.CLERK_WEBHOOK_SECRET ?? '')
|
|
241
|
+
let event: WebhookEvent
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
event = wh.verify(body, {
|
|
245
|
+
'svix-id': svix_id,
|
|
246
|
+
'svix-timestamp': svix_timestamp,
|
|
247
|
+
'svix-signature': svix_signature,
|
|
248
|
+
}) as WebhookEvent
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error('Error verifying webhook:', err)
|
|
251
|
+
return new Response('Error: Invalid signature', { status: 400 })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle events
|
|
255
|
+
const eventType = event.type
|
|
256
|
+
|
|
257
|
+
if (eventType === 'user.created') {
|
|
258
|
+
const { id, email_addresses, first_name, last_name, image_url } = event.data
|
|
259
|
+
|
|
260
|
+
await db.insert(users).values({
|
|
261
|
+
id,
|
|
262
|
+
email: email_addresses[0]?.email_address ?? '',
|
|
263
|
+
firstName: first_name ?? null,
|
|
264
|
+
lastName: last_name ?? null,
|
|
265
|
+
imageUrl: image_url ?? null,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
console.log(`User created: ${id}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (eventType === 'user.updated') {
|
|
272
|
+
const { id, email_addresses, first_name, last_name, image_url } = event.data
|
|
273
|
+
|
|
274
|
+
if (id) {
|
|
275
|
+
await db
|
|
276
|
+
.update(users)
|
|
277
|
+
.set({
|
|
278
|
+
email: email_addresses?.[0]?.email_address,
|
|
279
|
+
firstName: first_name,
|
|
280
|
+
lastName: last_name,
|
|
281
|
+
imageUrl: image_url,
|
|
282
|
+
updatedAt: new Date(),
|
|
283
|
+
})
|
|
284
|
+
.where(eq(users.id, id))
|
|
285
|
+
|
|
286
|
+
console.log(`User updated: ${id}`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (eventType === 'user.deleted') {
|
|
291
|
+
const { id } = event.data
|
|
292
|
+
|
|
293
|
+
if (id) {
|
|
294
|
+
await db.delete(users).where(eq(users.id, id))
|
|
295
|
+
|
|
296
|
+
console.log(`User deleted: ${id}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (eventType === 'organizationMembership.created' ||
|
|
301
|
+
eventType === 'organizationMembership.updated') {
|
|
302
|
+
const data = event.data as any
|
|
303
|
+
const userId = data.public_user_data
|
|
304
|
+
const orgId = data.organization
|
|
305
|
+
|
|
306
|
+
if (userId) {
|
|
307
|
+
await db
|
|
308
|
+
.update(users)
|
|
309
|
+
.set({
|
|
310
|
+
clerkOrgId: orgId,
|
|
311
|
+
updatedAt: new Date(),
|
|
312
|
+
})
|
|
313
|
+
.where(eq(users.id, userId))
|
|
314
|
+
|
|
315
|
+
console.log(`Organization membership updated for user: ${userId}, org: ${orgId}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return new Response(JSON.stringify({ received: true }), { status: 200 })
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('Error processing webhook:', error)
|
|
322
|
+
return new Response('Error: Webhook processing failed', { status: 500 })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Supported Webhook Events
|
|
328
|
+
|
|
329
|
+
| Event | Description | Handler Action |
|
|
330
|
+
|-------|-------------|-----------------|
|
|
331
|
+
| `user.created` | New user registered | Insert user into local database |
|
|
332
|
+
| `user.updated` | User profile updated | Update user in local database |
|
|
333
|
+
| `user.deleted` | User account deleted | Delete user from local database |
|
|
334
|
+
| `organizationMembership.created` | User joined organization | Update user's organization ID |
|
|
335
|
+
| `organizationMembership.updated` | Organization membership changed | Update user's organization ID |
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Protected Routes Configuration
|
|
340
|
+
|
|
341
|
+
### Middleware Setup
|
|
342
|
+
|
|
343
|
+
The middleware is located at [`src/middleware.ts`](../../src/middleware.ts):
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
|
347
|
+
|
|
348
|
+
const isPublicRoute = createRouteMatcher([
|
|
349
|
+
'/sign-in(.*)',
|
|
350
|
+
'/sign-up(.*)',
|
|
351
|
+
'/api/webhooks(.*)',
|
|
352
|
+
'/api/ping',
|
|
353
|
+
])
|
|
354
|
+
|
|
355
|
+
export default clerkMiddleware(async (auth, request) => {
|
|
356
|
+
if (!isPublicRoute(request)) {
|
|
357
|
+
await auth.protect()
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
export const config = {
|
|
362
|
+
matcher: [
|
|
363
|
+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
364
|
+
'/(api|trpc)(.*)',
|
|
365
|
+
],
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Public Routes
|
|
370
|
+
|
|
371
|
+
The following routes are public (no authentication required):
|
|
372
|
+
|
|
373
|
+
- `/sign-in` - Sign-in page
|
|
374
|
+
- `/sign-up` - Sign-up page
|
|
375
|
+
- `/api/webhooks/*` - Webhook endpoints
|
|
376
|
+
- `/api/ping` - Health check ping endpoint
|
|
377
|
+
|
|
378
|
+
### Protected Routes
|
|
379
|
+
|
|
380
|
+
All other routes require authentication. If a user tries to access a protected route without being authenticated, they will be redirected to the sign-in page.
|
|
381
|
+
|
|
382
|
+
### Adding Public Routes
|
|
383
|
+
|
|
384
|
+
To add a new public route, add it to the `isPublicRoute` matcher:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
const isPublicRoute = createRouteMatcher([
|
|
388
|
+
'/sign-in(.*)',
|
|
389
|
+
'/sign-up(.*)',
|
|
390
|
+
'/api/webhooks(.*)',
|
|
391
|
+
'/api/ping',
|
|
392
|
+
'/your-new-public-route(.*)', // Add new public route here
|
|
393
|
+
])
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Clerk Components Usage
|
|
399
|
+
|
|
400
|
+
### Sign-In Component
|
|
401
|
+
|
|
402
|
+
Located at [`src/app/(auth)/sign-in/[[...sign-in]]/page.tsx`](../../src/app/(auth)/sign-in/[[...sign-in]]/page.tsx):
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { SignIn } from '@clerk/nextjs'
|
|
406
|
+
|
|
407
|
+
export default function SignInPage() {
|
|
408
|
+
return (
|
|
409
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
410
|
+
<SignIn />
|
|
411
|
+
</div>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Sign-Up Component
|
|
417
|
+
|
|
418
|
+
Located at [`src/app/(auth)/sign-up/[[...sign-up]]/page.tsx`](../../src/app/(auth)/sign-up/[[...sign-up]]/page.tsx):
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { SignUp } from '@clerk/nextjs'
|
|
422
|
+
|
|
423
|
+
export default function SignUpPage() {
|
|
424
|
+
return (
|
|
425
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
426
|
+
<SignUp />
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### User Button Component
|
|
433
|
+
|
|
434
|
+
Used in the dashboard layout to show user menu:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { UserButton } from '@clerk/nextjs'
|
|
438
|
+
|
|
439
|
+
export function Navbar() {
|
|
440
|
+
return (
|
|
441
|
+
<header className="border-b">
|
|
442
|
+
<div className="container flex h-14 items-center justify-between">
|
|
443
|
+
<Link href="/health" className="font-semibold">DTT Framework</Link>
|
|
444
|
+
<UserButton afterSignOutUrl="/sign-in" />
|
|
445
|
+
</div>
|
|
446
|
+
</header>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### SignedIn and SignedOut Components
|
|
452
|
+
|
|
453
|
+
Conditionally render content based on authentication status:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { SignedIn, SignedOut } from '@clerk/nextjs'
|
|
457
|
+
|
|
458
|
+
export function MyComponent() {
|
|
459
|
+
return (
|
|
460
|
+
<div>
|
|
461
|
+
<SignedIn>
|
|
462
|
+
<p>You are signed in!</p>
|
|
463
|
+
</SignedIn>
|
|
464
|
+
<SignedOut>
|
|
465
|
+
<p>You are signed out!</p>
|
|
466
|
+
</SignedOut>
|
|
467
|
+
</div>
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Using Clerk Hooks
|
|
473
|
+
|
|
474
|
+
**useAuth Hook:**
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { useAuth } from '@clerk/nextjs'
|
|
478
|
+
|
|
479
|
+
export function MyComponent() {
|
|
480
|
+
const { userId, isLoaded, isSignedIn } = useAuth()
|
|
481
|
+
|
|
482
|
+
if (!isLoaded) {
|
|
483
|
+
return <div>Loading...</div>
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!isSignedIn) {
|
|
487
|
+
return <div>Not signed in</div>
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return <div>User ID: {userId}</div>
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**useUser Hook:**
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import { useUser } from '@clerk/nextjs'
|
|
498
|
+
|
|
499
|
+
export function UserProfile() {
|
|
500
|
+
const { user, isLoaded } = useUser()
|
|
501
|
+
|
|
502
|
+
if (!isLoaded) {
|
|
503
|
+
return <div>Loading...</div>
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<div>
|
|
508
|
+
<p>Name: {user?.fullName}</p>
|
|
509
|
+
<p>Email: {user?.primaryEmailAddress?.emailAddress}</p>
|
|
510
|
+
</div>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Server-Side Authentication
|
|
518
|
+
|
|
519
|
+
### Getting Auth Context
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { getAuth } from '@clerk/nextjs/server'
|
|
523
|
+
import { NextRequest } from 'next/server'
|
|
524
|
+
|
|
525
|
+
export async function GET(request: NextRequest) {
|
|
526
|
+
const auth = getAuth(request)
|
|
527
|
+
|
|
528
|
+
if (!auth.userId) {
|
|
529
|
+
return new Response('Unauthorized', { status: 401 })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return Response.json({ userId: auth.userId })
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### In API Routes (Hono)
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { getAuth } from '@clerk/nextjs/server'
|
|
540
|
+
import type { NextRequest } from 'next/server'
|
|
541
|
+
|
|
542
|
+
export const myRoute = new Hono()
|
|
543
|
+
|
|
544
|
+
myRoute.get('/protected', async (c) => {
|
|
545
|
+
const auth = getAuth(c.req.raw as NextRequest)
|
|
546
|
+
|
|
547
|
+
if (!auth.userId) {
|
|
548
|
+
return c.json({ error: 'Unauthorized' }, 401)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return c.json({ userId: auth.userId })
|
|
552
|
+
})
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### In Server Components
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { getAuth } from '@clerk/nextjs/server'
|
|
559
|
+
|
|
560
|
+
export default async function ServerComponent() {
|
|
561
|
+
const auth = getAuth()
|
|
562
|
+
|
|
563
|
+
if (!auth.userId) {
|
|
564
|
+
redirect('/sign-in')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return <div>Welcome, {auth.userId}</div>
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Clerk Health Checks
|
|
574
|
+
|
|
575
|
+
The framework includes health check endpoints for verifying Clerk integration:
|
|
576
|
+
|
|
577
|
+
### Health Check Endpoints
|
|
578
|
+
|
|
579
|
+
| Endpoint | Method | Description |
|
|
580
|
+
|----------|--------|-------------|
|
|
581
|
+
| `/api/health/clerk/user` | GET | Get current user |
|
|
582
|
+
| `/api/health/clerk/org` | GET | Get organization membership |
|
|
583
|
+
| `/api/health/clerk/members` | GET | List organization members |
|
|
584
|
+
|
|
585
|
+
### Example Response
|
|
586
|
+
|
|
587
|
+
```json
|
|
588
|
+
{
|
|
589
|
+
"status": "healthy",
|
|
590
|
+
"responseTimeMs": 45,
|
|
591
|
+
"message": "Successfully retrieved current user",
|
|
592
|
+
"data": {
|
|
593
|
+
"userId": "user_abc123",
|
|
594
|
+
"hasOrg": true
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Security Considerations
|
|
602
|
+
|
|
603
|
+
### Best Practices
|
|
604
|
+
|
|
605
|
+
1. **Never expose secret keys**: Only use `NEXT_PUBLIC_` prefixed keys in client code
|
|
606
|
+
2. **Verify webhook signatures**: Always verify webhook signatures to prevent spoofing
|
|
607
|
+
3. **Use environment variables**: Store all sensitive data in environment variables
|
|
608
|
+
4. **Enable MFA**: Require multi-factor authentication for sensitive operations
|
|
609
|
+
5. **Monitor webhook failures**: Set up alerts for failed webhook deliveries
|
|
610
|
+
|
|
611
|
+
### Session Management
|
|
612
|
+
|
|
613
|
+
- Clerk handles session management automatically
|
|
614
|
+
- Sessions are stored in secure HTTP-only cookies
|
|
615
|
+
- Session tokens are automatically refreshed
|
|
616
|
+
- Sessions can be revoked from the Clerk dashboard
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Troubleshooting
|
|
621
|
+
|
|
622
|
+
### Common Issues
|
|
623
|
+
|
|
624
|
+
**Issue: Webhook signature verification fails**
|
|
625
|
+
|
|
626
|
+
- Verify the `CLERK_WEBHOOK_SECRET` is correct
|
|
627
|
+
- Check that the webhook endpoint URL is correct
|
|
628
|
+
- Ensure the webhook is enabled in Clerk dashboard
|
|
629
|
+
|
|
630
|
+
**Issue: User not redirected after sign-in**
|
|
631
|
+
|
|
632
|
+
- Check `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` environment variable
|
|
633
|
+
- Verify middleware configuration
|
|
634
|
+
- Check browser console for errors
|
|
635
|
+
|
|
636
|
+
**Issue: Organization ID not syncing**
|
|
637
|
+
|
|
638
|
+
- Verify organization webhooks are enabled
|
|
639
|
+
- Check webhook logs in Clerk dashboard
|
|
640
|
+
- Ensure `organizationMembership.created` event is selected
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Related Documentation
|
|
645
|
+
|
|
646
|
+
- [Environment Variables](./environment-variables.md) - Clerk environment variables
|
|
647
|
+
- [API Layer](./api-layer.md) - API authentication middleware
|
|
648
|
+
- [Health Check System](./health-check-system.md) - Health check endpoints
|
|
649
|
+
- [Supabase Integration](./supabase-integration.md) - Database schema for users
|