@odvi/create-dtt-framework 0.1.7 → 0.1.9
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/utils/template.js +14 -1
- package/dist/utils/template.js.map +1 -1
- package/package.json +1 -1
- package/template/.env.example +117 -106
- package/template/DESIGN.md +1052 -0
- package/template/LICENSE +21 -0
- package/template/docs/framework/environment-variables.md +52 -0
- package/template/drizzle/0000_awesome_argent.sql +18 -0
- package/template/drizzle/meta/0000_snapshot.json +129 -0
- package/template/drizzle/meta/_journal.json +13 -0
- package/template/eslint.config.js +61 -0
- package/template/src/config/env.ts +5 -0
- package/template/src/features/health-check/config.ts +7 -0
- package/template/src/server/api/routes/health/aws-s3.ts +59 -0
- package/template/src/server/api/routes/health/index.ts +7 -3
- package/template/start-database.sh +88 -0
|
@@ -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
|