@amirulabu/create-recurring-rabbit-app 0.0.0-alpha
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/bin/index.js +2 -0
- package/dist/index.js +592 -0
- package/package.json +43 -0
- package/templates/default/.editorconfig +21 -0
- package/templates/default/.env.example +15 -0
- package/templates/default/.eslintrc.json +35 -0
- package/templates/default/.prettierrc.json +7 -0
- package/templates/default/README.md +346 -0
- package/templates/default/app.config.ts +20 -0
- package/templates/default/docs/adding-features.md +439 -0
- package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
- package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
- package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
- package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
- package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
- package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
- package/templates/default/docs/architecture.md +241 -0
- package/templates/default/docs/database.md +376 -0
- package/templates/default/docs/deployment.md +435 -0
- package/templates/default/docs/troubleshooting.md +668 -0
- package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
- package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
- package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
- package/templates/default/drizzle.config.ts +10 -0
- package/templates/default/lighthouserc.json +78 -0
- package/templates/default/src/app/__root.tsx +32 -0
- package/templates/default/src/app/api/auth/$.ts +15 -0
- package/templates/default/src/app/api/trpc.server.ts +12 -0
- package/templates/default/src/app/auth/forgot-password.tsx +107 -0
- package/templates/default/src/app/auth/login.tsx +34 -0
- package/templates/default/src/app/auth/register.tsx +34 -0
- package/templates/default/src/app/auth/reset-password.tsx +171 -0
- package/templates/default/src/app/auth/verify-email.tsx +111 -0
- package/templates/default/src/app/dashboard/index.tsx +122 -0
- package/templates/default/src/app/dashboard/settings.tsx +161 -0
- package/templates/default/src/app/globals.css +55 -0
- package/templates/default/src/app/index.tsx +83 -0
- package/templates/default/src/components/features/auth/login-form.tsx +172 -0
- package/templates/default/src/components/features/auth/register-form.tsx +202 -0
- package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
- package/templates/default/src/components/layout/header.tsx +29 -0
- package/templates/default/src/components/layout/sidebar.tsx +38 -0
- package/templates/default/src/components/ui/button.tsx +57 -0
- package/templates/default/src/components/ui/card.tsx +79 -0
- package/templates/default/src/components/ui/input.tsx +24 -0
- package/templates/default/src/lib/api.ts +42 -0
- package/templates/default/src/lib/auth.ts +292 -0
- package/templates/default/src/lib/email.ts +221 -0
- package/templates/default/src/lib/env.ts +119 -0
- package/templates/default/src/lib/hydration-timing.ts +289 -0
- package/templates/default/src/lib/monitoring.ts +336 -0
- package/templates/default/src/lib/utils.ts +6 -0
- package/templates/default/src/server/api/root.ts +10 -0
- package/templates/default/src/server/api/routers/dashboard.ts +37 -0
- package/templates/default/src/server/api/routers/user.ts +31 -0
- package/templates/default/src/server/api/trpc.ts +132 -0
- package/templates/default/src/server/auth/config.ts +241 -0
- package/templates/default/src/server/db/index.ts +153 -0
- package/templates/default/src/server/db/migrate.ts +125 -0
- package/templates/default/src/server/db/schema.ts +170 -0
- package/templates/default/src/server/db/seed.ts +130 -0
- package/templates/default/src/types/global.d.ts +25 -0
- package/templates/default/tailwind.config.js +46 -0
- package/templates/default/tsconfig.json +36 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Adding Features
|
|
2
|
+
|
|
3
|
+
## Creating New Database Tables
|
|
4
|
+
|
|
5
|
+
1. **Define schema** in `src/server/db/schema.ts`
|
|
6
|
+
2. **Generate migration** with `npm run db:generate`
|
|
7
|
+
3. **Apply migration** with `npm run db:migrate`
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
13
|
+
import { createId } from '@paralleldrive/cuid2'
|
|
14
|
+
|
|
15
|
+
export const posts = sqliteTable('posts', {
|
|
16
|
+
id: text('id')
|
|
17
|
+
.primaryKey()
|
|
18
|
+
.$defaultFn(() => createId()),
|
|
19
|
+
title: text('title').notNull(),
|
|
20
|
+
content: text('content').notNull(),
|
|
21
|
+
published: integer('published', { mode: 'boolean' }).default(false),
|
|
22
|
+
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
|
23
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(
|
|
24
|
+
() => new Date()
|
|
25
|
+
),
|
|
26
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(
|
|
27
|
+
() => new Date()
|
|
28
|
+
),
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Adding Relations
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { relations } from 'drizzle-orm'
|
|
36
|
+
|
|
37
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
38
|
+
posts: many(posts),
|
|
39
|
+
}))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Adding tRPC Procedures
|
|
43
|
+
|
|
44
|
+
1. **Create router** in `src/server/api/[feature].ts`
|
|
45
|
+
2. **Add to root router** in `src/server/api/root.ts`
|
|
46
|
+
3. **Use in components** via `api.[feature].[procedure].useQuery()`
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { z } from 'zod'
|
|
52
|
+
import { router, protectedProcedure } from '../trpc'
|
|
53
|
+
import { db } from '@/server/db'
|
|
54
|
+
import { posts, users } from '@/server/db/schema'
|
|
55
|
+
import { eq } from 'drizzle-orm'
|
|
56
|
+
|
|
57
|
+
export const postsRouter = router({
|
|
58
|
+
list: protectedProcedure.query(async ({ ctx }) => {
|
|
59
|
+
return await db.select().from(posts).where(eq(posts.userId, ctx.user.id))
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
create: protectedProcedure
|
|
63
|
+
.input(z.object({ title: z.string(), content: z.string() }))
|
|
64
|
+
.mutation(async ({ ctx, input }) => {
|
|
65
|
+
const result = await db
|
|
66
|
+
.insert(posts)
|
|
67
|
+
.values({
|
|
68
|
+
...input,
|
|
69
|
+
userId: ctx.user.id,
|
|
70
|
+
})
|
|
71
|
+
.returning()
|
|
72
|
+
return result[0]
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
update: protectedProcedure
|
|
76
|
+
.input(z.object({ id: z.string(), title: z.string(), content: z.string() }))
|
|
77
|
+
.mutation(async ({ ctx, input }) => {
|
|
78
|
+
const result = await db
|
|
79
|
+
.update(posts)
|
|
80
|
+
.set({
|
|
81
|
+
title: input.title,
|
|
82
|
+
content: input.content,
|
|
83
|
+
updatedAt: new Date(),
|
|
84
|
+
})
|
|
85
|
+
.where(eq(posts.id, input.id))
|
|
86
|
+
.returning()
|
|
87
|
+
return result[0]
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
delete: protectedProcedure
|
|
91
|
+
.input(z.object({ id: z.string() }))
|
|
92
|
+
.mutation(async ({ ctx, input }) => {
|
|
93
|
+
await db.delete(posts).where(eq(posts.id, input.id))
|
|
94
|
+
return { success: true }
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Add to root router:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { postsRouter } from './routers/posts'
|
|
103
|
+
|
|
104
|
+
export const appRouter = router({
|
|
105
|
+
user: userRouter,
|
|
106
|
+
dashboard: dashboardRouter,
|
|
107
|
+
posts: postsRouter,
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Creating New Pages
|
|
112
|
+
|
|
113
|
+
1. **Add route file** in `src/app/[route].tsx`
|
|
114
|
+
2. **Implement component** with TanStack Start conventions
|
|
115
|
+
3. **Add navigation** in layout components
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
121
|
+
|
|
122
|
+
export const Route = createFileRoute('/posts')({
|
|
123
|
+
component: PostsPage,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
function PostsPage() {
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
<h1>Posts</h1>
|
|
130
|
+
<PostsList />
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Protected Pages
|
|
137
|
+
|
|
138
|
+
Protected pages require authentication and use `beforeLoad` guards:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
142
|
+
import { api } from '@/lib/api'
|
|
143
|
+
|
|
144
|
+
export const Route = createFileRoute('/dashboard/settings')({
|
|
145
|
+
beforeLoad: async ({ context }) => {
|
|
146
|
+
if (!context.auth.user) {
|
|
147
|
+
throw redirect({ to: '/auth/login' })
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
component: SettingsPage,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
function SettingsPage() {
|
|
154
|
+
const { data: user } = api.user.getProfile.useQuery()
|
|
155
|
+
|
|
156
|
+
return <div>Settings for {user?.name}</div>
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Adding UI Components
|
|
161
|
+
|
|
162
|
+
### Using shadcn/ui Components
|
|
163
|
+
|
|
164
|
+
1. Install shadcn component: `npx shadcn-ui add [component]`
|
|
165
|
+
2. Use in your code with imports from `@/components/ui/[component]`
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx shadcn-ui add dialog
|
|
171
|
+
npx shadcn-ui add select
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
176
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
177
|
+
|
|
178
|
+
function MyComponent() {
|
|
179
|
+
return (
|
|
180
|
+
<Dialog>
|
|
181
|
+
<DialogContent>
|
|
182
|
+
<DialogHeader>
|
|
183
|
+
<DialogTitle>Select Option</DialogTitle>
|
|
184
|
+
</DialogHeader>
|
|
185
|
+
<Select>
|
|
186
|
+
<SelectTrigger>
|
|
187
|
+
<SelectValue />
|
|
188
|
+
</SelectTrigger>
|
|
189
|
+
<SelectContent>
|
|
190
|
+
<SelectItem value="option1">Option 1</SelectItem>
|
|
191
|
+
<SelectItem value="option2">Option 2</SelectItem>
|
|
192
|
+
</SelectContent>
|
|
193
|
+
</Select>
|
|
194
|
+
</DialogContent>
|
|
195
|
+
</Dialog>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Creating Feature-Specific Components
|
|
201
|
+
|
|
202
|
+
Create components in `src/components/features/[feature]/` to keep related code together:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Button } from '@/components/ui/button'
|
|
206
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
207
|
+
|
|
208
|
+
interface PostCardProps {
|
|
209
|
+
post: {
|
|
210
|
+
id: string
|
|
211
|
+
title: string
|
|
212
|
+
content: string
|
|
213
|
+
createdAt: Date
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function PostCard({ post }: PostCardProps) {
|
|
218
|
+
return (
|
|
219
|
+
<Card>
|
|
220
|
+
<CardHeader>
|
|
221
|
+
<CardTitle>{post.title}</CardTitle>
|
|
222
|
+
</CardHeader>
|
|
223
|
+
<CardContent>
|
|
224
|
+
<p>{post.content}</p>
|
|
225
|
+
<p className="text-sm text-gray-500">{new Date(post.createdAt).toLocaleDateString()}</p>
|
|
226
|
+
</CardContent>
|
|
227
|
+
</Card>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Data Fetching Patterns
|
|
233
|
+
|
|
234
|
+
### Query with tRPC
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
function PostsList() {
|
|
238
|
+
const { data: posts, isLoading, error } = api.posts.list.useQuery()
|
|
239
|
+
|
|
240
|
+
if (isLoading) return <div>Loading...</div>
|
|
241
|
+
if (error) return <div>Error loading posts</div>
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div>
|
|
245
|
+
{posts?.map((post) => (
|
|
246
|
+
<PostCard key={post.id} post={post} />
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Mutation with tRPC
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
function CreatePost() {
|
|
257
|
+
const createPost = api.posts.create.useMutation({
|
|
258
|
+
onSuccess: () => {
|
|
259
|
+
api.posts.list.invalidate()
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const handleSubmit = (data: { title: string, content: string }) => {
|
|
264
|
+
createPost.mutate(data)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<form onSubmit={(e) => {
|
|
269
|
+
e.preventDefault()
|
|
270
|
+
const formData = new FormData(e.currentTarget)
|
|
271
|
+
handleSubmit({
|
|
272
|
+
title: String(formData.get('title')),
|
|
273
|
+
content: String(formData.get('content')),
|
|
274
|
+
})
|
|
275
|
+
}}>
|
|
276
|
+
{/* form fields */}
|
|
277
|
+
</form>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Optimistic Updates
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const deletePost = api.posts.delete.useMutation({
|
|
286
|
+
onMutate: async (deletedId) => {
|
|
287
|
+
await queryClient.cancelQueries({ queryKey: ['posts.list'] })
|
|
288
|
+
const previousPosts = queryClient.getQueryData(['posts.list']) as Post[]
|
|
289
|
+
|
|
290
|
+
queryClient.setQueryData(
|
|
291
|
+
['posts.list'],
|
|
292
|
+
previousPosts?.filter((p) => p.id !== deletedId.id)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return { previousPosts }
|
|
296
|
+
},
|
|
297
|
+
onError: (err, variables, context) => {
|
|
298
|
+
queryClient.setQueryData(['posts.list'], context?.previousPosts)
|
|
299
|
+
},
|
|
300
|
+
onSettled: () => {
|
|
301
|
+
queryClient.invalidateQueries({ queryKey: ['posts.list'] })
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Common Patterns
|
|
307
|
+
|
|
308
|
+
### Form Handling
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { useState } from 'react'
|
|
312
|
+
|
|
313
|
+
function MyForm() {
|
|
314
|
+
const [formData, setFormData] = useState({ name: '', email: '' })
|
|
315
|
+
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
316
|
+
const [loading, setLoading] = useState(false)
|
|
317
|
+
|
|
318
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
319
|
+
e.preventDefault()
|
|
320
|
+
setLoading(true)
|
|
321
|
+
setErrors({})
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await api.myProcedure.mutateAsync(formData)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
setErrors({ submit: 'An error occurred' })
|
|
327
|
+
} finally {
|
|
328
|
+
setLoading(false)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<form onSubmit={handleSubmit}>
|
|
334
|
+
{/* form fields */}
|
|
335
|
+
{errors.submit && <div className="error">{errors.submit}</div>}
|
|
336
|
+
</form>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Error Handling
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
function MyComponent() {
|
|
345
|
+
const { data, error, isLoading } = api.myProcedure.useQuery()
|
|
346
|
+
|
|
347
|
+
if (error) {
|
|
348
|
+
return (
|
|
349
|
+
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
|
|
350
|
+
<p className="text-red-600">Error: {error.message}</p>
|
|
351
|
+
<button onClick={() => api.myProcedure.refetch()}>Retry</button>
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// render content
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Loading States
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
function MyComponent() {
|
|
364
|
+
const { data, isLoading } = api.myProcedure.useQuery()
|
|
365
|
+
|
|
366
|
+
if (isLoading) {
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex items-center justify-center p-8">
|
|
369
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
370
|
+
<span className="ml-2">Loading...</span>
|
|
371
|
+
</div>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return <div>{data?.content}</div>
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Adding Third-Party Integrations
|
|
380
|
+
|
|
381
|
+
### Stripe for Payments
|
|
382
|
+
|
|
383
|
+
1. Install Stripe SDK: `npm install stripe`
|
|
384
|
+
2. Add environment variable: `STRIPE_SECRET_KEY`
|
|
385
|
+
3. Create webhook handler in `src/app/api/stripe-webhook.ts`
|
|
386
|
+
4. Add procedures to handle payments
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import Stripe from 'stripe'
|
|
390
|
+
|
|
391
|
+
const stripe = new Stripe(env.STRIPE_SECRET_KEY)
|
|
392
|
+
|
|
393
|
+
export const paymentRouter = router({
|
|
394
|
+
createCheckout: protectedProcedure
|
|
395
|
+
.input(z.object({ priceId: z.string() }))
|
|
396
|
+
.mutation(async ({ ctx, input }) => {
|
|
397
|
+
const session = await stripe.checkout.sessions.create({
|
|
398
|
+
customer_email: ctx.user.email,
|
|
399
|
+
mode: 'payment',
|
|
400
|
+
line_items: [{ price: input.priceId, quantity: 1 }],
|
|
401
|
+
success_url: `${env.PUBLIC_APP_URL}/success`,
|
|
402
|
+
cancel_url: `${env.PUBLIC_APP_URL}/cancel`,
|
|
403
|
+
})
|
|
404
|
+
return { url: session.url }
|
|
405
|
+
}),
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Email Service Extensions
|
|
410
|
+
|
|
411
|
+
Customize email templates in `src/lib/email-templates/`:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { Resend } from 'resend'
|
|
415
|
+
|
|
416
|
+
const resend = new Resend(env.RESEND_API_KEY)
|
|
417
|
+
|
|
418
|
+
await resend.emails.send({
|
|
419
|
+
from: 'your-app@yourdomain.com',
|
|
420
|
+
to: 'user@example.com',
|
|
421
|
+
subject: 'Welcome!',
|
|
422
|
+
html: '<h1>Welcome to our app</h1>',
|
|
423
|
+
})
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Testing New Features
|
|
427
|
+
|
|
428
|
+
1. **Add table** → `npm run db:generate && npm run db:migrate`
|
|
429
|
+
2. **Add tRPC procedure** → Test in tRPC DevTools
|
|
430
|
+
3. **Add UI component** → Test in isolation
|
|
431
|
+
4. **Add page** → Test navigation and rendering
|
|
432
|
+
5. **Update types** → Verify type safety throughout
|
|
433
|
+
|
|
434
|
+
Best practices:
|
|
435
|
+
|
|
436
|
+
- Test each layer independently (database → tRPC → UI)
|
|
437
|
+
- Use TypeScript to catch errors at compile time
|
|
438
|
+
- Add loading and error states for better UX
|
|
439
|
+
- Invalidate queries after mutations for data consistency
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-001: Use SQLite for Development Database
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need development database with zero external dependencies but can migrate to production PostgreSQL
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use SQLite for local development with automatic file creation
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: Instant setup, no Docker/external services needed
|
|
18
|
+
- **Negative**: Some PostgreSQL features not available in development
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Auto-create data/app.db if DATABASE_URL not provided, use same Drizzle schema for both SQLite and PostgreSQL
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-002: Use TanStack Start over Next.js
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need modern React framework with SSR and file-based routing
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use TanStack Start instead of Next.js
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: Better TypeScript integration, file-based routing without App Router complexity, smaller bundle size
|
|
18
|
+
- **Negative**: Smaller community, less documentation
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Configure Vinxi bundler, file-based routing in src/app/, SSR enabled by default
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-003: Use Better-auth over NextAuth
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need authentication library with email/password, session management, and TypeScript support
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use Better-auth instead of NextAuth
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: Simpler configuration for basic email/password auth, better TypeScript support, database session storage built-in
|
|
18
|
+
- **Negative**: Less mature, smaller ecosystem
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Configure with Drizzle adapter, email verification flow, session management
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-004: Use Drizzle over Prisma
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need ORM with type safety and migration management for micro-SaaS
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use Drizzle ORM instead of Prisma
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: Zero runtime overhead with compile-time query building, better migration control with SQL-first approach, lighter weight
|
|
18
|
+
- **Negative**: Less intuitive API, fewer built-in features
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Define schema in src/server/db/schema.ts, use drizzle-kit for migrations
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-005: Use tRPC for API Layer
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need type-safe API with zero manual interface definitions
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use tRPC v11 for API layer
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: End-to-end type safety from database to UI, automatic type inference, zero code generation
|
|
18
|
+
- **Negative**: Learning curve, different patterns from REST APIs
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Define routers in src/server/api/routers/, integrate with TanStack Query
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR-006: Use Tailwind CSS v4 with shadcn/ui
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Need styling system with rapid development and professional components
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Use Tailwind CSS v4 with shadcn/ui components
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
- **Positive**: Rapid development, consistent design system, no need for custom CSS
|
|
18
|
+
- **Negative**: Large HTML classes, potential bundle size increase
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
Configure Tailwind v4, install shadcn/ui components, use for all UI
|