@odvi/create-dtt-framework 0.1.6 → 0.1.8

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.
@@ -0,0 +1,1052 @@
1
+ # DTT Framework - Design Document
2
+
3
+ ## Overview
4
+
5
+ A production-ready Next.js boilerplate with integrated services, featuring a health check dashboard to verify all connections are working. This serves as the foundation for enterprise applications at ODVI.
6
+
7
+ ---
8
+
9
+ ## Tech Stack
10
+
11
+ | Layer | Technology | Purpose |
12
+ |-------|------------|---------|
13
+ | **Framework** | Next.js latest (App Router) | Full-stack React framework |
14
+ | **Language** | TypeScript | Type safety |
15
+ | **Styling** | Tailwind CSS + Shadcn/ui | Utility-first CSS + component primitives |
16
+ | **Auth** | Clerk | Authentication & user management |
17
+ | **Database** | Supabase (PostgreSQL) | Primary transactional database |
18
+ | **ORM** | Drizzle | Type-safe database access |
19
+ | **Storage** | Supabase Storage | File uploads |
20
+ | **Edge Functions** | Supabase Edge Functions | Serverless compute |
21
+ | **API Layer** | Hono | Lightweight API framework |
22
+ | **Server State** | TanStack Query | Data fetching & caching |
23
+ | **Client State** | Zustand | UI state management |
24
+ | **Data Warehouse** | Snowflake | Analytics & reporting |
25
+ | **Core Banking** | NextBank API | Banking operations (placeholder) |
26
+
27
+ ---
28
+
29
+ ## Services to Integrate
30
+
31
+ ### 1. Clerk Authentication
32
+ - User sign-up / sign-in (built-in Clerk components)
33
+ - Organization membership
34
+ - Session management
35
+ - Webhook sync to local DB
36
+
37
+ ### 2. Supabase Database
38
+ - PostgreSQL via Drizzle ORM
39
+ - Connection pooling (Transaction mode)
40
+ - CRUD operations test
41
+
42
+ ### 3. Supabase Storage
43
+ - File upload / download
44
+ - Signed URLs
45
+ - Bucket management
46
+
47
+ ### 4. Supabase Edge Functions
48
+ - Serverless function invocation
49
+ - Auth header passthrough
50
+
51
+ ### 5. Snowflake Data Warehouse
52
+ - Connection test via `snowflake-sdk`
53
+ - Query execution
54
+ - Warehouse/database verification
55
+
56
+ ### 6. NextBank API (Placeholder)
57
+ - API connectivity check
58
+ - Authentication test
59
+ - Basic endpoint verification
60
+
61
+ ---
62
+
63
+ ## Folder Structure
64
+
65
+ ```
66
+ src/
67
+ ├── app/
68
+ │ ├── (auth)/
69
+ │ │ ├── sign-in/[[...sign-in]]/
70
+ │ │ │ └── page.tsx # Clerk <SignIn /> component
71
+ │ │ ├── sign-up/[[...sign-up]]/
72
+ │ │ │ └── page.tsx # Clerk <SignUp /> component
73
+ │ │ └── layout.tsx
74
+ │ │
75
+ │ ├── (dashboard)/
76
+ │ │ ├── health/
77
+ │ │ │ └── page.tsx # Health check dashboard
78
+ │ │ ├── layout.tsx
79
+ │ │ └── page.tsx # Dashboard index
80
+ │ │
81
+ │ ├── api/
82
+ │ │ ├── [[...route]]/
83
+ │ │ │ └── route.ts # Hono catch-all
84
+ │ │ └── webhooks/
85
+ │ │ └── clerk/
86
+ │ │ └── route.ts # Clerk webhook handler
87
+ │ │
88
+ │ ├── layout.tsx
89
+ │ ├── page.tsx # Landing / redirect to sign-in
90
+ │ └── providers.tsx
91
+
92
+
93
+ ├── server/
94
+ │ ├── api/
95
+ │ │ ├── index.ts # Hono app instance
96
+ │ │ ├── middleware/
97
+ │ │ │ ├── auth.ts # Clerk auth middleware for Hono
98
+ │ │ │ └── logger.ts
99
+ │ │ └── routes/
100
+ │ │ ├── health/
101
+ │ │ │ ├── index.ts # Aggregates all health routes
102
+ │ │ │ ├── clerk.ts
103
+ │ │ │ ├── database.ts
104
+ │ │ │ ├── storage.ts
105
+ │ │ │ ├── edge-functions.ts
106
+ │ │ │ ├── snowflake.ts
107
+ │ │ │ └── nextbank.ts # Placeholder
108
+ │ │ └── users.ts
109
+ │ │
110
+ │ └── db/
111
+ │ ├── index.ts # Drizzle client
112
+ │ ├── schema/
113
+ │ │ ├── index.ts
114
+ │ │ ├── users.ts
115
+ │ │ └── health-checks.ts
116
+ │ ├── queries/
117
+ │ │ └── users.ts
118
+ │ └── migrations/
119
+
120
+
121
+ ├── hooks/
122
+ │ ├── queries/
123
+ │ │ └── use-health-checks.ts
124
+ │ └── utils/
125
+ │ └── use-debounce.ts
126
+
127
+
128
+ ├── stores/
129
+ │ └── ui-store.ts
130
+
131
+
132
+ ├── components/
133
+ │ ├── ui/ # Shadcn primitives
134
+ │ │ ├── button.tsx
135
+ │ │ ├── card.tsx
136
+ │ │ ├── badge.tsx
137
+ │ │ ├── collapsible.tsx
138
+ │ │ └── [...]
139
+ │ │
140
+ │ ├── layouts/
141
+ │ │ └── navbar.tsx
142
+ │ │
143
+ │ └── shared/
144
+ │ └── loading-spinner.tsx
145
+
146
+
147
+ ├── features/
148
+ │ └── health-check/
149
+ │ ├── components/
150
+ │ │ └── health-dashboard.tsx
151
+ │ ├── config.ts
152
+ │ ├── types.ts
153
+ │ └── index.ts
154
+
155
+
156
+ ├── lib/
157
+ │ ├── supabase/
158
+ │ │ ├── client.ts
159
+ │ │ ├── server.ts
160
+ │ │ └── admin.ts
161
+ │ │
162
+ │ ├── snowflake/
163
+ │ │ └── client.ts
164
+ │ │
165
+ │ ├── nextbank/
166
+ │ │ └── client.ts # Placeholder
167
+ │ │
168
+ │ ├── utils.ts
169
+ │ └── validators.ts
170
+
171
+
172
+ ├── types/
173
+ │ └── index.ts
174
+
175
+
176
+ └── config/
177
+ ├── env.ts
178
+ └── site.ts
179
+
180
+ # Root files
181
+ ├── drizzle.config.ts
182
+ ├── middleware.ts
183
+ ├── tailwind.config.ts
184
+ ├── tsconfig.json
185
+ ├── .env.local
186
+ └── .env.example
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Environment Variables
192
+
193
+ ```bash
194
+ # .env.example
195
+
196
+ # App
197
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
198
+
199
+ # Clerk
200
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
201
+ CLERK_SECRET_KEY=sk_test_xxx
202
+ CLERK_WEBHOOK_SECRET=whsec_xxx
203
+ NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
204
+ NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
205
+ NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/health
206
+ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/health
207
+
208
+ # Supabase
209
+ NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
210
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx
211
+ SUPABASE_SERVICE_ROLE_KEY=eyJxxx
212
+ DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
213
+
214
+ # Snowflake
215
+ SNOWFLAKE_ACCOUNT=xxx.us-east-1
216
+ SNOWFLAKE_USERNAME=xxx
217
+ SNOWFLAKE_PASSWORD=xxx
218
+ SNOWFLAKE_WAREHOUSE=COMPUTE_WH
219
+ SNOWFLAKE_DATABASE=ANALYTICS
220
+ SNOWFLAKE_SCHEMA=PUBLIC
221
+ SNOWFLAKE_ROLE=ANALYST
222
+
223
+ # NextBank (placeholder)
224
+ NEXTBANK_API=https://api.nextbank.com
225
+ NEXTBANK_API_USERNAME=user
226
+ NEXTBANK_API_PASSWORD=pass
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Database Schema
232
+
233
+ ### users.ts
234
+ ```typescript
235
+ // src/server/db/schema/users.ts
236
+ import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
237
+
238
+ export const users = pgTable('users', {
239
+ id: text('id').primaryKey(), // Clerk user ID
240
+ email: varchar('email', { length: 255 }).notNull().unique(),
241
+ firstName: varchar('first_name', { length: 255 }),
242
+ lastName: varchar('last_name', { length: 255 }),
243
+ imageUrl: text('image_url'),
244
+ clerkOrgId: text('clerk_org_id'),
245
+ createdAt: timestamp('created_at').defaultNow().notNull(),
246
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
247
+ })
248
+
249
+ export type User = typeof users.$inferSelect
250
+ export type NewUser = typeof users.$inferInsert
251
+ ```
252
+
253
+ ### health-checks.ts
254
+ ```typescript
255
+ // src/server/db/schema/health-checks.ts
256
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
257
+
258
+ export const healthCheckTests = pgTable('health_check_tests', {
259
+ id: uuid('id').primaryKey().defaultRandom(),
260
+ testKey: text('test_key').notNull(),
261
+ testValue: text('test_value'),
262
+ createdAt: timestamp('created_at').defaultNow().notNull(),
263
+ })
264
+
265
+ export type HealthCheckTest = typeof healthCheckTests.$inferSelect
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Health Check API Endpoints
271
+
272
+ | Service | Endpoint | Method | Description |
273
+ |---------|----------|--------|-------------|
274
+ | **Clerk** | `/api/health/clerk/user` | GET | Get current user |
275
+ | **Clerk** | `/api/health/clerk/org` | GET | Get org membership |
276
+ | **Clerk** | `/api/health/clerk/members` | GET | List org members |
277
+ | **Database** | `/api/health/database/write` | POST | Write test row |
278
+ | **Database** | `/api/health/database/read` | GET | Read test row |
279
+ | **Database** | `/api/health/database/delete` | DELETE | Delete test row |
280
+ | **Storage** | `/api/health/storage/upload` | POST | Upload test file |
281
+ | **Storage** | `/api/health/storage/download` | GET | Download test file |
282
+ | **Storage** | `/api/health/storage/delete` | DELETE | Delete test file |
283
+ | **Edge Functions** | `/api/health/edge/ping` | GET | Ping edge function |
284
+ | **Edge Functions** | `/api/health/edge/auth` | GET | Test auth header |
285
+ | **Snowflake** | `/api/health/snowflake/connect` | GET | Test connection |
286
+ | **Snowflake** | `/api/health/snowflake/query` | GET | Execute test query |
287
+ | **NextBank** | `/api/health/nextbank/ping` | GET | Ping API |
288
+ | **All** | `/api/health/all` | GET | Run all checks |
289
+
290
+ ---
291
+
292
+ ## Key Implementation Files
293
+
294
+ ### 1. Hono App Setup
295
+
296
+ ```typescript
297
+ // src/server/api/index.ts
298
+ import { Hono } from 'hono'
299
+ import { cors } from 'hono/cors'
300
+ import { logger } from 'hono/logger'
301
+ import { authMiddleware } from './middleware/auth'
302
+ import { healthRoutes } from './routes/health'
303
+ import { usersRoutes } from './routes/users'
304
+
305
+ const app = new Hono().basePath('/api')
306
+
307
+ app.use('*', logger())
308
+ app.use('*', cors())
309
+
310
+ // Public routes
311
+ app.get('/ping', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }))
312
+
313
+ // Protected routes
314
+ app.use('*', authMiddleware)
315
+ app.route('/health', healthRoutes)
316
+ app.route('/users', usersRoutes)
317
+
318
+ export { app }
319
+ export type AppType = typeof app
320
+ ```
321
+
322
+ ### 2. Hono Mount in Next.js
323
+
324
+ ```typescript
325
+ // src/app/api/[[...route]]/route.ts
326
+ import { handle } from 'hono/vercel'
327
+ import { app } from '@/server/api'
328
+
329
+ export const runtime = 'nodejs'
330
+
331
+ export const GET = handle(app)
332
+ export const POST = handle(app)
333
+ export const PUT = handle(app)
334
+ export const DELETE = handle(app)
335
+ export const PATCH = handle(app)
336
+ ```
337
+
338
+ ### 3. Auth Middleware for Hono
339
+
340
+ ```typescript
341
+ // src/server/api/middleware/auth.ts
342
+ import { createMiddleware } from 'hono/factory'
343
+ import { getAuth } from '@clerk/nextjs/server'
344
+ import type { NextRequest } from 'next/server'
345
+
346
+ export const authMiddleware = createMiddleware(async (c, next) => {
347
+ const request = c.req.raw as NextRequest
348
+ const auth = getAuth(request)
349
+
350
+ if (!auth.userId) {
351
+ return c.json({ error: 'Unauthorized' }, 401)
352
+ }
353
+
354
+ c.set('auth', auth)
355
+ c.set('userId', auth.userId)
356
+ c.set('orgId', auth.orgId)
357
+
358
+ await next()
359
+ })
360
+ ```
361
+
362
+ ### 4. Health Routes Aggregation
363
+
364
+ ```typescript
365
+ // src/server/api/routes/health/index.ts
366
+ import { Hono } from 'hono'
367
+ import { clerkHealthRoutes } from './clerk'
368
+ import { databaseHealthRoutes } from './database'
369
+ import { storageHealthRoutes } from './storage'
370
+ import { edgeFunctionsHealthRoutes } from './edge-functions'
371
+ import { snowflakeHealthRoutes } from './snowflake'
372
+ import { nextbankHealthRoutes } from './nextbank'
373
+
374
+ export const healthRoutes = new Hono()
375
+
376
+ healthRoutes.route('/clerk', clerkHealthRoutes)
377
+ healthRoutes.route('/database', databaseHealthRoutes)
378
+ healthRoutes.route('/storage', storageHealthRoutes)
379
+ healthRoutes.route('/edge', edgeFunctionsHealthRoutes)
380
+ healthRoutes.route('/snowflake', snowflakeHealthRoutes)
381
+ healthRoutes.route('/nextbank', nextbankHealthRoutes)
382
+
383
+ healthRoutes.get('/all', async (c) => {
384
+ // Run all checks in parallel and return aggregated results
385
+ return c.json({ timestamp: new Date().toISOString(), services: {} })
386
+ })
387
+ ```
388
+
389
+ ### 5. Drizzle Client
390
+
391
+ ```typescript
392
+ // src/server/db/index.ts
393
+ import { drizzle } from 'drizzle-orm/postgres-js'
394
+ import postgres from 'postgres'
395
+ import * as schema from './schema'
396
+ import { env } from '@/config/env'
397
+
398
+ const client = postgres(env.DATABASE_URL, {
399
+ prepare: false // Required for Supabase Transaction pooling
400
+ })
401
+
402
+ export const db = drizzle(client, { schema })
403
+ ```
404
+
405
+ ### 6. Snowflake Client
406
+
407
+ ```typescript
408
+ // src/lib/snowflake/client.ts
409
+ import snowflake from 'snowflake-sdk'
410
+ import { env } from '@/config/env'
411
+
412
+ const config = {
413
+ account: env.SNOWFLAKE_ACCOUNT,
414
+ username: env.SNOWFLAKE_USERNAME,
415
+ password: env.SNOWFLAKE_PASSWORD,
416
+ warehouse: env.SNOWFLAKE_WAREHOUSE,
417
+ database: env.SNOWFLAKE_DATABASE,
418
+ schema: env.SNOWFLAKE_SCHEMA,
419
+ role: env.SNOWFLAKE_ROLE,
420
+ }
421
+
422
+ export function createSnowflakeConnection() {
423
+ return snowflake.createConnection(config)
424
+ }
425
+
426
+ export async function connectSnowflake(): Promise<snowflake.Connection> {
427
+ return new Promise((resolve, reject) => {
428
+ const connection = createSnowflakeConnection()
429
+ connection.connect((err, conn) => {
430
+ if (err) reject(err)
431
+ else resolve(conn)
432
+ })
433
+ })
434
+ }
435
+
436
+ export async function executeQuery<T = unknown>(
437
+ connection: snowflake.Connection,
438
+ sqlText: string,
439
+ binds?: unknown[]
440
+ ): Promise<T[]> {
441
+ return new Promise((resolve, reject) => {
442
+ connection.execute({
443
+ sqlText,
444
+ binds,
445
+ complete: (err, stmt, rows) => {
446
+ if (err) reject(err)
447
+ else resolve((rows || []) as T[])
448
+ },
449
+ })
450
+ })
451
+ }
452
+
453
+ export async function destroyConnection(connection: snowflake.Connection): Promise<void> {
454
+ return new Promise((resolve, reject) => {
455
+ connection.destroy((err) => {
456
+ if (err) reject(err)
457
+ else resolve()
458
+ })
459
+ })
460
+ }
461
+ ```
462
+
463
+ ### 7. Snowflake Health Routes
464
+
465
+ ```typescript
466
+ // src/server/api/routes/health/snowflake.ts
467
+ import { Hono } from 'hono'
468
+ import { connectSnowflake, executeQuery, destroyConnection } from '@/lib/snowflake/client'
469
+
470
+ export const snowflakeHealthRoutes = new Hono()
471
+
472
+ snowflakeHealthRoutes.get('/connect', async (c) => {
473
+ const start = performance.now()
474
+
475
+ try {
476
+ const connection = await connectSnowflake()
477
+ await destroyConnection(connection)
478
+
479
+ return c.json({
480
+ status: 'healthy',
481
+ responseTimeMs: Math.round(performance.now() - start),
482
+ message: 'Successfully connected to Snowflake',
483
+ })
484
+ } catch (error) {
485
+ return c.json({
486
+ status: 'error',
487
+ responseTimeMs: Math.round(performance.now() - start),
488
+ error: error instanceof Error ? error.message : 'Connection failed',
489
+ }, 500)
490
+ }
491
+ })
492
+
493
+ snowflakeHealthRoutes.get('/query', async (c) => {
494
+ const start = performance.now()
495
+
496
+ try {
497
+ const connection = await connectSnowflake()
498
+ const rows = await executeQuery<{ CURRENT_TIMESTAMP: string }>(
499
+ connection,
500
+ 'SELECT CURRENT_TIMESTAMP()'
501
+ )
502
+ await destroyConnection(connection)
503
+
504
+ return c.json({
505
+ status: 'healthy',
506
+ responseTimeMs: Math.round(performance.now() - start),
507
+ message: 'Query executed successfully',
508
+ data: { timestamp: rows[0]?.CURRENT_TIMESTAMP },
509
+ })
510
+ } catch (error) {
511
+ return c.json({
512
+ status: 'error',
513
+ responseTimeMs: Math.round(performance.now() - start),
514
+ error: error instanceof Error ? error.message : 'Query failed',
515
+ }, 500)
516
+ }
517
+ })
518
+ ```
519
+
520
+ ### 8. NextBank Client (Placeholder)
521
+
522
+ ```typescript
523
+ // src/lib/nextbank/client.ts
524
+ import { env } from '@/config/env'
525
+
526
+ class NextBankClient {
527
+ private apiUrl: string
528
+ private username: string
529
+ private password: string
530
+
531
+ constructor() {
532
+ this.apiUrl = env.NEXTBANK_API ?? ''
533
+ this.username = env.NEXTBANK_API_USERNAME ?? ''
534
+ this.password = env.NEXTBANK_API_PASSWORD ?? ''
535
+ }
536
+
537
+ async ping(fingerprint: string): Promise<{ status: string; timestamp: string }> {
538
+ // TODO: Implement actual NextBank API ping
539
+ const response = await fetch(`${this.apiUrl}/management/status`, {
540
+ method: 'POST',
541
+ headers: {
542
+ Authorization: 'Basic ' + btoa(`${this.username}:${this.password}`),
543
+ 'Content-Type': 'application/json',
544
+ 'User-Agent': 'dtt-framework-health-check',
545
+ },
546
+ body: JSON.stringify({
547
+ fingerprint,
548
+ }),
549
+ })
550
+ if (!response.ok) throw new Error(`NextBank API error: ${response.status}`)
551
+ return response.json()
552
+ }
553
+ }
554
+
555
+ export const nextbankClient = new NextBankClient()
556
+ ```
557
+
558
+ ### 9. NextBank Health Routes (Placeholder)
559
+
560
+ ```typescript
561
+ // src/server/api/routes/health/nextbank.ts
562
+ import { Hono } from 'hono'
563
+ import { nextbankClient } from '@/lib/nextbank/client'
564
+ import { env } from '@/config/env'
565
+
566
+ export const nextbankHealthRoutes = new Hono()
567
+
568
+ nextbankHealthRoutes.get('/ping', async (c) => {
569
+ const start = performance.now()
570
+
571
+ if (!env.NEXTBANK_API) {
572
+ return c.json({
573
+ status: 'unconfigured',
574
+ responseTimeMs: Math.round(performance.now() - start),
575
+ message: 'NextBank API not configured',
576
+ })
577
+ }
578
+
579
+ try {
580
+ const result = await nextbankClient.ping()
581
+ return c.json({
582
+ status: 'healthy',
583
+ responseTimeMs: Math.round(performance.now() - start),
584
+ data: result,
585
+ })
586
+ } catch (error) {
587
+ return c.json({
588
+ status: 'error',
589
+ responseTimeMs: Math.round(performance.now() - start),
590
+ error: error instanceof Error ? error.message : 'Ping failed',
591
+ }, 500)
592
+ }
593
+ })
594
+ ```
595
+
596
+ ### 10. Environment Validation
597
+
598
+ ```typescript
599
+ // src/config/env.ts
600
+ import { z } from 'zod'
601
+
602
+ const envSchema = z.object({
603
+ NEXT_PUBLIC_APP_URL: z.string().url(),
604
+
605
+ // Clerk
606
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
607
+ CLERK_SECRET_KEY: z.string().startsWith('sk_'),
608
+ CLERK_WEBHOOK_SECRET: z.string().optional(),
609
+
610
+ // Supabase
611
+ NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
612
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(),
613
+ SUPABASE_SERVICE_ROLE_KEY: z.string(),
614
+ DATABASE_URL: z.string(),
615
+
616
+ // Snowflake
617
+ SNOWFLAKE_ACCOUNT: z.string(),
618
+ SNOWFLAKE_USERNAME: z.string(),
619
+ SNOWFLAKE_PASSWORD: z.string(),
620
+ SNOWFLAKE_WAREHOUSE: z.string(),
621
+ SNOWFLAKE_DATABASE: z.string(),
622
+ SNOWFLAKE_SCHEMA: z.string(),
623
+ SNOWFLAKE_ROLE: z.string(),
624
+
625
+ // NextBank (optional)
626
+ NEXTBANK_API: z.string().url().optional(),
627
+ NEXTBANK_API_USERNAME: z.string().optional(),
628
+ NEXTBANK_API_PASSWORD: z.string().optional(),
629
+ })
630
+
631
+ export const env = envSchema.parse(process.env)
632
+ export type Env = z.infer<typeof envSchema>
633
+ ```
634
+
635
+ ---
636
+
637
+ ## Auth Pages (Clerk Built-in Components)
638
+
639
+ ```typescript
640
+ // src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
641
+ import { SignIn } from '@clerk/nextjs'
642
+
643
+ export default function SignInPage() {
644
+ return (
645
+ <div className="flex min-h-screen items-center justify-center">
646
+ <SignIn />
647
+ </div>
648
+ )
649
+ }
650
+ ```
651
+
652
+ ```typescript
653
+ // src/app/(auth)/sign-up/[[...sign-up]]/page.tsx
654
+ import { SignUp } from '@clerk/nextjs'
655
+
656
+ export default function SignUpPage() {
657
+ return (
658
+ <div className="flex min-h-screen items-center justify-center">
659
+ <SignUp />
660
+ </div>
661
+ )
662
+ }
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Health Check Dashboard
668
+
669
+ ### Overview
670
+
671
+ The Health Check Dashboard provides a comprehensive monitoring interface for all integrated services. It features:
672
+
673
+ - **Overall System Status Card**: Shows aggregate health status with response time
674
+ - **"Run All" Button**: Executes all health checks with a counter showing total number of checks
675
+ - **Individual Service Cards**: Each service has its own card with status badge
676
+ - **Individual Check Buttons**: Each health check has a dedicated "Run Check" button
677
+ - **Real-time Results**: Check results are displayed below each button with detailed information
678
+ - **Loading States**: Visual feedback while checks are in progress
679
+
680
+ ### Types
681
+
682
+ ```typescript
683
+ // src/features/health-check/types.ts
684
+ export type HealthStatus = 'healthy' | 'unhealthy' | 'error' | 'pending' | 'unconfigured'
685
+
686
+ export interface ServiceCheck {
687
+ name: string
688
+ endpoint: string
689
+ status: HealthStatus
690
+ responseTimeMs?: number
691
+ error?: string
692
+ }
693
+
694
+ export interface ServiceHealth {
695
+ name: string
696
+ icon: string
697
+ status: HealthStatus
698
+ responseTimeMs: number
699
+ checks: ServiceCheck[]
700
+ }
701
+
702
+ export interface IndividualCheckResult {
703
+ name: string
704
+ status: HealthStatus
705
+ responseTimeMs?: number
706
+ error?: string
707
+ httpStatus?: number
708
+ timestamp?: string
709
+ }
710
+ ```
711
+
712
+ ### Service Configuration
713
+
714
+ ```typescript
715
+ // src/features/health-check/config.ts
716
+ export const SERVICES = [
717
+ {
718
+ name: 'Clerk Authentication',
719
+ icon: 'key',
720
+ checks: [
721
+ { name: 'Get Current User', endpoint: '/clerk/user' },
722
+ { name: 'Get Org Membership', endpoint: '/clerk/org' },
723
+ { name: 'List Org Members', endpoint: '/clerk/members' },
724
+ ],
725
+ },
726
+ {
727
+ name: 'Supabase Database',
728
+ icon: 'database',
729
+ checks: [
730
+ { name: 'Write Test Row', endpoint: '/database/write' },
731
+ { name: 'Read Test Row', endpoint: '/database/read' },
732
+ { name: 'Delete Test Row', endpoint: '/database/delete' },
733
+ ],
734
+ },
735
+ {
736
+ name: 'Supabase Storage',
737
+ icon: 'folder',
738
+ checks: [
739
+ { name: 'Upload Test File', endpoint: '/storage/upload' },
740
+ { name: 'Download Test File', endpoint: '/storage/download' },
741
+ { name: 'Delete Test File', endpoint: '/storage/delete' },
742
+ ],
743
+ },
744
+ {
745
+ name: 'Supabase Edge Functions',
746
+ icon: 'zap',
747
+ checks: [
748
+ { name: 'Ping Edge Function', endpoint: '/edge/ping' },
749
+ { name: 'Test Auth Header', endpoint: '/edge/auth' },
750
+ ],
751
+ },
752
+ {
753
+ name: 'Snowflake',
754
+ icon: 'snowflake',
755
+ checks: [
756
+ { name: 'Test Connection', endpoint: '/snowflake/connect' },
757
+ { name: 'Execute Query', endpoint: '/snowflake/query' },
758
+ ],
759
+ },
760
+ {
761
+ name: 'NextBank',
762
+ icon: 'building',
763
+ checks: [
764
+ { name: 'Ping API', endpoint: '/nextbank/ping' },
765
+ ],
766
+ },
767
+ ] as const
768
+ ```
769
+
770
+ ### Dashboard Component
771
+
772
+ ```typescript
773
+ // src/features/health-check/components/health-dashboard.tsx
774
+ 'use client'
775
+
776
+ import { useHealthChecks } from '@/hooks/queries/use-health-checks'
777
+ import { SERVICES } from '@/features/health-check/config'
778
+ import { LoadingSpinner } from '@/components/shared/loading-spinner'
779
+ import { Button } from '@/components/ui/button'
780
+ import { Card } from '@/components/ui/card'
781
+ import { Badge } from '@/components/ui/badge'
782
+ import { CheckCircle, XCircle, AlertCircle, Clock, Shield, Play, RefreshCw } from 'lucide-react'
783
+ import { useState } from 'react'
784
+ import type { HealthStatus } from '@/features/health-check/types'
785
+
786
+ const statusIcons: Record<HealthStatus, React.ReactNode> = {
787
+ healthy: <CheckCircle className="h-5 w-5 text-green-500" />,
788
+ unhealthy: <XCircle className="h-5 w-5 text-red-500" />,
789
+ error: <XCircle className="h-5 w-5 text-red-500" />,
790
+ pending: <Clock className="h-5 w-5 text-yellow-500" />,
791
+ unconfigured: <Shield className="h-5 w-5 text-gray-400" />,
792
+ }
793
+
794
+ const statusColors: Record<HealthStatus, string> = {
795
+ healthy: 'bg-green-100 text-green-800 border-green-200',
796
+ unhealthy: 'bg-red-100 text-red-800 border-red-200',
797
+ error: 'bg-red-100 text-red-800 border-red-200',
798
+ pending: 'bg-yellow-100 text-yellow-800 border-yellow-200',
799
+ unconfigured: 'bg-gray-100 text-gray-800 border-gray-200',
800
+ }
801
+
802
+ export function HealthDashboard() {
803
+ const { data, isLoading, error, refetch } = useHealthChecks()
804
+ const [individualChecks, setIndividualChecks] = useState<Record<string, IndividualCheckResult>>({})
805
+ const [loadingChecks, setLoadingChecks] = useState<Set<string>>(new Set())
806
+
807
+ const totalHealthChecks = SERVICES.reduce((total, service) => total + service.checks.length, 0)
808
+
809
+ const runIndividualCheck = async (serviceName: string, checkName: string, endpoint: string) => {
810
+ const checkKey = `${serviceName}-${checkName}`
811
+ setLoadingChecks((prev) => new Set(prev).add(checkKey))
812
+
813
+ try {
814
+ const response = await fetch(`/api/health${endpoint}`)
815
+ const result = await response.json()
816
+
817
+ setIndividualChecks((prev) => ({
818
+ ...prev,
819
+ [checkKey]: {
820
+ name: checkName,
821
+ status: result.status || (response.ok ? 'healthy' : 'error'),
822
+ responseTimeMs: result.responseTimeMs,
823
+ error: result.error,
824
+ httpStatus: response.status,
825
+ timestamp: new Date().toISOString(),
826
+ },
827
+ }))
828
+ } catch (err) {
829
+ setIndividualChecks((prev) => ({
830
+ ...prev,
831
+ [checkKey]: {
832
+ name: checkName,
833
+ status: 'error',
834
+ error: err instanceof Error ? err.message : 'Check failed',
835
+ timestamp: new Date().toISOString(),
836
+ },
837
+ }))
838
+ } finally {
839
+ setLoadingChecks((prev) => {
840
+ const next = new Set(prev)
841
+ next.delete(checkKey)
842
+ return next
843
+ })
844
+ }
845
+ }
846
+
847
+ const runAllChecks = async () => {
848
+ refetch()
849
+ // Also run individual checks for better UX
850
+ for (const service of SERVICES) {
851
+ for (const check of service.checks) {
852
+ await runIndividualCheck(service.name, check.name, check.endpoint)
853
+ }
854
+ }
855
+ }
856
+
857
+ // ... UI rendering code
858
+ }
859
+ ```
860
+
861
+ ### Dashboard Page
862
+
863
+ ```typescript
864
+ // src/app/(dashboard)/health/page.tsx
865
+ import { HealthDashboard } from '@/features/health-check'
866
+
867
+ export default function HealthPage() {
868
+ return (
869
+ <div className="container max-w-4xl py-8">
870
+ <div className="mb-8">
871
+ <h1 className="text-2xl font-semibold">Health Check Dashboard</h1>
872
+ <p className="text-muted-foreground">
873
+ Verify all services are connected and working correctly.
874
+ </p>
875
+ </div>
876
+ <HealthDashboard />
877
+ </div>
878
+ )
879
+ }
880
+ ```
881
+
882
+ ### Dashboard Layout
883
+
884
+ ```typescript
885
+ // src/app/(dashboard)/layout.tsx
886
+ import { UserButton } from '@clerk/nextjs'
887
+ import Link from 'next/link'
888
+
889
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
890
+ return (
891
+ <div className="min-h-screen bg-background">
892
+ <header className="border-b">
893
+ <div className="container flex h-14 items-center justify-between">
894
+ <Link href="/health" className="font-semibold">DTT Framework</Link>
895
+ <UserButton afterSignOutUrl="/sign-in" />
896
+ </div>
897
+ </header>
898
+ <main>{children}</main>
899
+ </div>
900
+ )
901
+ }
902
+ ```
903
+
904
+ ### Dashboard Features
905
+
906
+ 1. **Overall Status Card**
907
+ - Shows aggregate health status of all services
908
+ - Displays total response time for all checks
909
+ - "Run All Checks" button with counter showing total number of health checks
910
+ - Last updated timestamp
911
+
912
+ 2. **Individual Service Cards**
913
+ - Each service has its own card with status badge
914
+ - Shows number of checks for the service
915
+ - Displays service-level health status
916
+
917
+ 3. **Individual Check Buttons**
918
+ - Each health check has a dedicated "Run Check" button
919
+ - Loading spinner while check is in progress
920
+ - Button is disabled during execution
921
+
922
+ 4. **Check Results Display**
923
+ - Results appear below each check button
924
+ - Shows status badge with color coding
925
+ - Displays response time in milliseconds
926
+ - Shows error message if check failed
927
+ - Displays HTTP status code
928
+ - Shows timestamp of when check was executed
929
+
930
+ 5. **Status Indicators**
931
+ - Green checkmark for healthy status
932
+ - Red X for error/unhealthy status
933
+ - Yellow clock for pending status
934
+ - Gray shield for unconfigured status
935
+
936
+ ---
937
+
938
+ ## Providers
939
+
940
+ ```typescript
941
+ // src/app/providers.tsx
942
+ 'use client'
943
+
944
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
945
+ import { ClerkProvider } from '@clerk/nextjs'
946
+ import { useState } from 'react'
947
+
948
+ export function Providers({ children }: { children: React.ReactNode }) {
949
+ const [queryClient] = useState(() => new QueryClient({
950
+ defaultOptions: { queries: { staleTime: 60 * 1000, retry: 1 } },
951
+ }))
952
+
953
+ return (
954
+ <ClerkProvider>
955
+ <QueryClientProvider client={queryClient}>
956
+ {children}
957
+ </QueryClientProvider>
958
+ </ClerkProvider>
959
+ )
960
+ }
961
+ ```
962
+
963
+ ---
964
+
965
+ ## Clerk Middleware
966
+
967
+ ```typescript
968
+ // src/middleware.ts
969
+ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
970
+
971
+ const isPublicRoute = createRouteMatcher([
972
+ '/sign-in(.*)',
973
+ '/sign-up(.*)',
974
+ '/api/webhooks(.*)',
975
+ '/api/ping',
976
+ ])
977
+
978
+ export default clerkMiddleware(async (auth, request) => {
979
+ if (!isPublicRoute(request)) {
980
+ await auth.protect()
981
+ }
982
+ })
983
+
984
+ export const config = {
985
+ matcher: [
986
+ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
987
+ '/(api|trpc)(.*)',
988
+ ],
989
+ }
990
+ ```
991
+
992
+ ---
993
+
994
+ ## Setup Commands
995
+
996
+ ```bash
997
+ # Create Next.js app
998
+ npx create-next-app@latest odvi-boilerplate --typescript --tailwind --eslint --app --src-dir
999
+
1000
+ # Core dependencies
1001
+ pnpm add hono @clerk/nextjs
1002
+ pnpm add @supabase/supabase-js @supabase/ssr
1003
+ pnpm add drizzle-orm postgres
1004
+ pnpm add @tanstack/react-query zustand
1005
+ pnpm add snowflake-sdk
1006
+ pnpm add zod svix
1007
+
1008
+ # Dev dependencies
1009
+ pnpm add -D drizzle-kit @types/snowflake-sdk
1010
+
1011
+ # Shadcn
1012
+ pnpm dlx shadcn@latest init
1013
+ pnpm dlx shadcn@latest add button card badge collapsible
1014
+
1015
+ # Database
1016
+ pnpm drizzle-kit generate
1017
+ pnpm drizzle-kit push
1018
+ ```
1019
+
1020
+ ---
1021
+
1022
+ ## Drizzle Config
1023
+
1024
+ ```typescript
1025
+ // drizzle.config.ts
1026
+ import { defineConfig } from 'drizzle-kit'
1027
+
1028
+ export default defineConfig({
1029
+ schema: './src/server/db/schema/index.ts',
1030
+ out: './src/server/db/migrations',
1031
+ dialect: 'postgresql',
1032
+ dbCredentials: { url: process.env.DATABASE_URL! },
1033
+ })
1034
+ ```
1035
+
1036
+ ---
1037
+
1038
+ ## Summary
1039
+
1040
+ | Service | Status | Health Checks |
1041
+ |---------|--------|---------------|
1042
+ | Clerk Authentication | Implemented | User, Org, Members |
1043
+ | Supabase Database | Implemented | Write, Read, Delete |
1044
+ | Supabase Storage | Implemented | Upload, Download, Delete |
1045
+ | Supabase Edge Functions | Implemented | Ping, Auth Header |
1046
+ | Snowflake | Implemented | Connect, Query |
1047
+ | NextBank | Placeholder | Ping |
1048
+
1049
+ The health check dashboard uses:
1050
+ - **Clerk built-in components** for auth (`<SignIn />`, `<SignUp />`, `<UserButton />`)
1051
+ - **Shadcn primitives** for UI (`Card`, `Button`, `Badge`, `Collapsible`)
1052
+ - **Lucide icons** for service indicators