@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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* This module configures the server-side authentication system using better-auth.
|
|
5
|
+
* It handles user authentication, session management, email verification, and
|
|
6
|
+
* password reset functionality.
|
|
7
|
+
*
|
|
8
|
+
* KEY AUTHENTICATION FLOWS:
|
|
9
|
+
* 1. Email/Password Registration - Users register with email and password, must verify email before access
|
|
10
|
+
* 2. Email/Password Sign In - Verified users can sign in with credentials
|
|
11
|
+
* 3. Email Verification - Users receive verification link via email to activate account
|
|
12
|
+
* 4. Password Reset - Users can request password reset via email link
|
|
13
|
+
*
|
|
14
|
+
* SECURITY CONSIDERATIONS:
|
|
15
|
+
* - Email verification is REQUIRED before account access (prevents fake/bot accounts)
|
|
16
|
+
* - Sessions expire after 7 days with daily refresh (limits credential exposure window)
|
|
17
|
+
* - Secret key must be strong and kept in environment variables (never in code)
|
|
18
|
+
* - Password reset links are single-use and time-limited
|
|
19
|
+
* - CSRF protection handled by better-auth through same-site cookies
|
|
20
|
+
* - Session cookies have strict prefix to avoid conflicts with other cookies
|
|
21
|
+
*
|
|
22
|
+
* DATABASE PATTERN:
|
|
23
|
+
* Uses Drizzle ORM adapter with dual database support:
|
|
24
|
+
* - PostgreSQL: For production (uses DATABASE_URL)
|
|
25
|
+
* - SQLite: For development/testing (fallback)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { betterAuth } from 'better-auth'
|
|
29
|
+
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
30
|
+
import { tanstackStartCookies } from 'better-auth/tanstack-start'
|
|
31
|
+
import { db } from '@/server/db'
|
|
32
|
+
import { users, sessions, accounts, verifications } from '@/server/db/schema'
|
|
33
|
+
import { env } from '@/lib/env'
|
|
34
|
+
import { sendVerificationEmail, sendPasswordResetEmail } from '@/lib/email'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Main authentication instance configured for the application.
|
|
38
|
+
*
|
|
39
|
+
* This instance is exported and used throughout the application to handle
|
|
40
|
+
* all server-side authentication operations including sign in, sign out,
|
|
41
|
+
* session verification, and token generation.
|
|
42
|
+
*
|
|
43
|
+
* CONFIGURATION DECISIONS:
|
|
44
|
+
* - Database Adapter: Drizzle ORM allows flexible database switching (PostgreSQL/SQLite)
|
|
45
|
+
* - Email Verification Required: Prevents spam accounts and ensures users own their email
|
|
46
|
+
* - Session Duration: 7 days balances user convenience with security
|
|
47
|
+
* - Cookie Cache: 5-minute cache reduces database hits for session lookups
|
|
48
|
+
* - Cross-subdomain cookies disabled: Prevents session leakage between subdomains
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { auth } from '@/server/auth/config'
|
|
53
|
+
*
|
|
54
|
+
* // Verify session from request
|
|
55
|
+
* const session = await auth.api.getSession({
|
|
56
|
+
* headers: request.headers
|
|
57
|
+
* })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const auth = betterAuth({
|
|
61
|
+
/**
|
|
62
|
+
* Database configuration using Drizzle adapter.
|
|
63
|
+
*
|
|
64
|
+
* The adapter bridges better-auth with the application's database schema.
|
|
65
|
+
* Using 'postgres' provider for production DATABASE_URL, falls back to 'sqlite'.
|
|
66
|
+
*
|
|
67
|
+
* SECURITY: Database credentials must be protected via environment variables.
|
|
68
|
+
*/
|
|
69
|
+
database: drizzleAdapter(db, {
|
|
70
|
+
provider: env.DATABASE_URL ? 'pg' : 'sqlite',
|
|
71
|
+
schema: {
|
|
72
|
+
user: users,
|
|
73
|
+
session: sessions,
|
|
74
|
+
account: accounts,
|
|
75
|
+
verification: verifications,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
/**
|
|
79
|
+
* Base URL for the authentication server.
|
|
80
|
+
*
|
|
81
|
+
* Used for constructing callback URLs, verification links, and reset password links.
|
|
82
|
+
* Must match the domain where auth endpoints are hosted.
|
|
83
|
+
*
|
|
84
|
+
* SECURITY: Must be HTTPS in production to prevent token interception.
|
|
85
|
+
*/
|
|
86
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
87
|
+
/**
|
|
88
|
+
* Secret key for signing and encrypting tokens.
|
|
89
|
+
*
|
|
90
|
+
* Used to sign JWT tokens and encrypt sensitive session data.
|
|
91
|
+
* This is a critical security value - if compromised, all sessions can be forged.
|
|
92
|
+
*
|
|
93
|
+
* SECURITY REQUIREMENTS:
|
|
94
|
+
* - Must be at least 32 characters long
|
|
95
|
+
* - Must be cryptographically random
|
|
96
|
+
* - Must never be committed to version control
|
|
97
|
+
* - Must be rotated periodically in production
|
|
98
|
+
*/
|
|
99
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
100
|
+
/**
|
|
101
|
+
* Email and password authentication configuration.
|
|
102
|
+
*
|
|
103
|
+
* Enables traditional email/password authentication flow with required
|
|
104
|
+
* email verification for security.
|
|
105
|
+
*
|
|
106
|
+
* SECURITY CONSIDERATIONS:
|
|
107
|
+
* - Email verification prevents fake/throwaway accounts
|
|
108
|
+
* - Passwords are hashed automatically by better-auth before storage
|
|
109
|
+
* - Verification and reset links are time-limited by default
|
|
110
|
+
*/
|
|
111
|
+
emailAndPassword: {
|
|
112
|
+
enabled: true,
|
|
113
|
+
/**
|
|
114
|
+
* Requires email verification before users can sign in.
|
|
115
|
+
*
|
|
116
|
+
* This ensures users own the email address they register with and
|
|
117
|
+
* prevents automated account creation by bots.
|
|
118
|
+
*/
|
|
119
|
+
requireEmailVerification: true,
|
|
120
|
+
/**
|
|
121
|
+
* Sends email verification link to new users.
|
|
122
|
+
*
|
|
123
|
+
* @param user - The user object containing email and user ID
|
|
124
|
+
* @param url - The verification URL that user must click
|
|
125
|
+
*
|
|
126
|
+
* SECURITY: URL contains signed token that expires, preventing replay attacks.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* User receives email with link like:
|
|
130
|
+
* https://example.com/verify?token=abc123...
|
|
131
|
+
*/
|
|
132
|
+
sendVerificationEmail: async ({
|
|
133
|
+
user,
|
|
134
|
+
url,
|
|
135
|
+
}: {
|
|
136
|
+
user: { email: string }
|
|
137
|
+
url: string
|
|
138
|
+
}) => {
|
|
139
|
+
await sendVerificationEmail(user.email, url)
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Sends password reset email to users who forgot their password.
|
|
143
|
+
*
|
|
144
|
+
* @param user - The user object containing email and user ID
|
|
145
|
+
* @param url - The password reset URL that user must click
|
|
146
|
+
*
|
|
147
|
+
* SECURITY:
|
|
148
|
+
* - Link is single-use and expires automatically
|
|
149
|
+
* - User must verify email before requesting reset
|
|
150
|
+
* - Old password is invalidated upon successful reset
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* User receives email with link like:
|
|
154
|
+
* https://example.com/reset-password?token=xyz789...
|
|
155
|
+
*/
|
|
156
|
+
sendResetPassword: async ({
|
|
157
|
+
user,
|
|
158
|
+
url,
|
|
159
|
+
}: {
|
|
160
|
+
user: { email: string }
|
|
161
|
+
url: string
|
|
162
|
+
}) => {
|
|
163
|
+
await sendPasswordResetEmail(user.email, url)
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Session configuration controlling authentication persistence.
|
|
168
|
+
*
|
|
169
|
+
* SECURITY RATIONALE:
|
|
170
|
+
* - 7-day expiration: Long enough for user convenience, short enough to limit exposure
|
|
171
|
+
* - 24-hour update: Refreshes session daily, invalidating old tokens
|
|
172
|
+
* - Cookie cache: Reduces database load while maintaining security
|
|
173
|
+
*/
|
|
174
|
+
session: {
|
|
175
|
+
/**
|
|
176
|
+
* Session expires after 7 days (604800 seconds).
|
|
177
|
+
*
|
|
178
|
+
* SECURITY: Shorter sessions are more secure but require more frequent sign-ins.
|
|
179
|
+
* 7 days is a reasonable balance for most applications.
|
|
180
|
+
*/
|
|
181
|
+
expiresIn: 60 * 60 * 24 * 7,
|
|
182
|
+
/**
|
|
183
|
+
* Session age update interval (86400 seconds = 24 hours).
|
|
184
|
+
*
|
|
185
|
+
* Updates the session expiration time every 24 hours while the user
|
|
186
|
+
* remains active. This extends the session while keeping it fresh.
|
|
187
|
+
*
|
|
188
|
+
* SECURITY: Regular refreshes limit the window for stolen session tokens.
|
|
189
|
+
*/
|
|
190
|
+
updateAge: 60 * 60 * 24,
|
|
191
|
+
/**
|
|
192
|
+
* Cookie cache configuration to reduce database queries.
|
|
193
|
+
*
|
|
194
|
+
* Caches session data in cookies for 5 minutes to avoid hitting the
|
|
195
|
+
* database on every request while maintaining session validity.
|
|
196
|
+
*
|
|
197
|
+
* SECURITY:
|
|
198
|
+
* - Cache duration (5 min) is short enough to be secure
|
|
199
|
+
* - Falls back to database on cache miss
|
|
200
|
+
* - Does not store sensitive data in cache
|
|
201
|
+
*/
|
|
202
|
+
cookieCache: {
|
|
203
|
+
enabled: true,
|
|
204
|
+
maxAge: 60 * 5,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Advanced configuration options for edge cases.
|
|
209
|
+
*/
|
|
210
|
+
advanced: {
|
|
211
|
+
/**
|
|
212
|
+
* Cross-subdomain cookie configuration.
|
|
213
|
+
*
|
|
214
|
+
* Disabled for security to prevent session cookie from being accessible
|
|
215
|
+
* across subdomains. This isolates sessions to specific domains.
|
|
216
|
+
*
|
|
217
|
+
* SECURITY: If enabled, a compromised subdomain could hijack session.
|
|
218
|
+
*/
|
|
219
|
+
crossSubDomainCookies: {
|
|
220
|
+
enabled: false,
|
|
221
|
+
},
|
|
222
|
+
/**
|
|
223
|
+
* Cookie prefix for all authentication cookies.
|
|
224
|
+
*
|
|
225
|
+
* Adds a prefix to all better-auth cookies to prevent conflicts with
|
|
226
|
+
* other cookies in the same domain.
|
|
227
|
+
*
|
|
228
|
+
* SECURITY: Prevents cookie collisions and makes cookies easily identifiable.
|
|
229
|
+
*/
|
|
230
|
+
cookiePrefix: 'better-auth',
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
* Plugins configuration.
|
|
234
|
+
*
|
|
235
|
+
* Extends better-auth functionality with additional features.
|
|
236
|
+
*
|
|
237
|
+
* IMPORTANT: tanstackStartCookies plugin must be the LAST plugin in the array
|
|
238
|
+
* to ensure proper cookie handling for TanStack Start.
|
|
239
|
+
*/
|
|
240
|
+
plugins: [tanstackStartCookies()],
|
|
241
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Connection Module
|
|
3
|
+
*
|
|
4
|
+
* This module provides the database connection instance and connection management.
|
|
5
|
+
* It abstracts the differences between SQLite (development) and PostgreSQL (production)
|
|
6
|
+
* through a unified Drizzle ORM interface.
|
|
7
|
+
*
|
|
8
|
+
* Database Design Decisions:
|
|
9
|
+
* - SQLite with WAL mode for development: Better concurrency and performance
|
|
10
|
+
* - PostgreSQL for production: Enterprise-grade scaling, reliability, and features
|
|
11
|
+
* - Connection pooling for PostgreSQL with configurable limits
|
|
12
|
+
* - Singleton pattern: One database connection instance per application lifecycle
|
|
13
|
+
*
|
|
14
|
+
* Migration Strategy:
|
|
15
|
+
* - Use drizzle-kit to generate migration files from schema changes
|
|
16
|
+
* - Development: `npx drizzle-kit push` for rapid schema changes
|
|
17
|
+
* - Production: `pnpm db:migrate` for versioned migrations
|
|
18
|
+
*
|
|
19
|
+
* Development vs Production:
|
|
20
|
+
* - Development:
|
|
21
|
+
* - SQLite database stored in `data/app.db` by default
|
|
22
|
+
* - WAL mode enabled for better read/write concurrency
|
|
23
|
+
* - Synchronous mode set to NORMAL for faster writes (less durability)
|
|
24
|
+
* - Debug logging function registered for query profiling
|
|
25
|
+
* - Production:
|
|
26
|
+
* - PostgreSQL connection via DATABASE_URL
|
|
27
|
+
* - Connection pool with max 20 connections
|
|
28
|
+
* - 30-second idle timeout to free unused connections
|
|
29
|
+
* - 60-second connection timeout for reliability
|
|
30
|
+
*
|
|
31
|
+
* Connection Management:
|
|
32
|
+
* - Database connection is initialized at module load time
|
|
33
|
+
* - Use closeDatabaseConnection() for graceful shutdown (testing, server restart)
|
|
34
|
+
* - Never manually close the connection during normal operation
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
38
|
+
import { drizzle as drizzlePostgres } from 'drizzle-orm/postgres-js'
|
|
39
|
+
import Database from 'better-sqlite3'
|
|
40
|
+
import postgres from 'postgres'
|
|
41
|
+
import { env } from '@/lib/env'
|
|
42
|
+
import * as schema from './schema'
|
|
43
|
+
|
|
44
|
+
const isProduction = env.NODE_ENV === 'production'
|
|
45
|
+
|
|
46
|
+
let sqlite: Database.Database | null = null
|
|
47
|
+
let postgresClient: postgres.Sql | null = null
|
|
48
|
+
|
|
49
|
+
let db: ReturnType<typeof drizzle> | ReturnType<typeof drizzlePostgres>
|
|
50
|
+
|
|
51
|
+
if (isProduction && env.DATABASE_URL) {
|
|
52
|
+
postgresClient = postgres(env.DATABASE_URL, {
|
|
53
|
+
max: 20,
|
|
54
|
+
idle_timeout: 30,
|
|
55
|
+
connect_timeout: 60,
|
|
56
|
+
})
|
|
57
|
+
db = drizzlePostgres(postgresClient, { schema })
|
|
58
|
+
} else {
|
|
59
|
+
const dbPath = env.DATABASE_URL ?? 'data/app.db'
|
|
60
|
+
sqlite = new Database(dbPath)
|
|
61
|
+
sqlite.pragma('journal_mode = WAL')
|
|
62
|
+
|
|
63
|
+
if (env.NODE_ENV === 'development') {
|
|
64
|
+
sqlite.pragma('synchronous = NORMAL')
|
|
65
|
+
sqlite.function('profile', () => {
|
|
66
|
+
console.log('[SQLite] Query executed')
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
db = drizzle(sqlite, { schema })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Database instance
|
|
75
|
+
*
|
|
76
|
+
* Unified Drizzle ORM instance that works with both SQLite and PostgreSQL.
|
|
77
|
+
* The database type is determined at build time based on NODE_ENV environment variable.
|
|
78
|
+
* Use this instance to perform all database operations through type-safe
|
|
79
|
+
* Drizzle query builder.
|
|
80
|
+
*
|
|
81
|
+
* Type safety: Uses conditional types to ensure type safety based on the build-time
|
|
82
|
+
* environment (development: SQLite, production: PostgreSQL). All db.select(), db.insert(),
|
|
83
|
+
* db.update(), and db.delete() operations are fully typed without union type conflicts.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* import { db } from './db'
|
|
87
|
+
* import { users } from './db/schema'
|
|
88
|
+
*
|
|
89
|
+
* const allUsers = await db.select().from(users)
|
|
90
|
+
* const newUser = await db.insert(users).values({ email: 'test@example.com', name: 'Test', emailVerified: true }).returning()
|
|
91
|
+
* const updatedUser = await db.update(users).set({ name: 'Updated' }).where(eq(users.id, 'id')).returning()
|
|
92
|
+
*/
|
|
93
|
+
export { db }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Type guard to check if database is PostgreSQL
|
|
97
|
+
*
|
|
98
|
+
* Useful for branching logic when database-specific features are needed.
|
|
99
|
+
*
|
|
100
|
+
* @returns true if PostgreSQL, false if SQLite
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* if (isPostgres()) {
|
|
104
|
+
* // PostgreSQL-specific code
|
|
105
|
+
* } else {
|
|
106
|
+
* // SQLite-specific code
|
|
107
|
+
* }
|
|
108
|
+
*/
|
|
109
|
+
export function isPostgres(): boolean {
|
|
110
|
+
return isProduction && env.DATABASE_URL !== undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gracefully closes the database connection
|
|
115
|
+
*
|
|
116
|
+
* This function should be called when shutting down the application,
|
|
117
|
+
* typically in server shutdown handlers or test teardown. It properly
|
|
118
|
+
* closes both PostgreSQL and SQLite connections, releasing any
|
|
119
|
+
* held resources.
|
|
120
|
+
*
|
|
121
|
+
* Side Effects:
|
|
122
|
+
* - Closes the PostgreSQL connection pool if in production
|
|
123
|
+
* - Closes the SQLite database file handle if in development
|
|
124
|
+
* - Sets the internal connection instances to null
|
|
125
|
+
*
|
|
126
|
+
* Usage:
|
|
127
|
+
* - Application shutdown: Call when server receives termination signal
|
|
128
|
+
* - Test teardown: Call in afterAll() to clean up test database
|
|
129
|
+
* - Server restart: Call before reinitializing the connection
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* // In server shutdown handler
|
|
133
|
+
* process.on('SIGTERM', async () => {
|
|
134
|
+
* await closeDatabaseConnection()
|
|
135
|
+
* process.exit(0)
|
|
136
|
+
* })
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // In test teardown
|
|
140
|
+
* afterAll(async () => {
|
|
141
|
+
* await closeDatabaseConnection()
|
|
142
|
+
* })
|
|
143
|
+
*
|
|
144
|
+
* @returns {Promise<void>} Resolves when all connections are closed
|
|
145
|
+
*/
|
|
146
|
+
export async function closeDatabaseConnection() {
|
|
147
|
+
if (postgresClient) {
|
|
148
|
+
await postgresClient.end()
|
|
149
|
+
}
|
|
150
|
+
if (sqlite) {
|
|
151
|
+
sqlite.close()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Migration Module
|
|
3
|
+
*
|
|
4
|
+
* This module handles database schema migrations using Drizzle ORM's migration system.
|
|
5
|
+
* It provides the runMigrations function to apply pending migrations to the database.
|
|
6
|
+
*
|
|
7
|
+
* Database Design Decisions:
|
|
8
|
+
* - Migrations are SQL files generated by drizzle-kit from schema changes
|
|
9
|
+
* - Migration files store a journal of applied migrations in the database
|
|
10
|
+
* - Supports both SQLite (development) and PostgreSQL (production) via unified API
|
|
11
|
+
*
|
|
12
|
+
* Migration Strategy:
|
|
13
|
+
* - Generate migrations: `npx drizzle-kit generate`
|
|
14
|
+
* - Run migrations: Use this module's runMigrations() function
|
|
15
|
+
* - For complex multi-statement migrations, use the CLI: `pnpm db:migrate`
|
|
16
|
+
* - For simple schema changes during development, use: `npx drizzle-kit push`
|
|
17
|
+
*
|
|
18
|
+
* Development vs Production:
|
|
19
|
+
* - Development:
|
|
20
|
+
* - SQLite migrations run in a single transaction
|
|
21
|
+
* - Multi-statement SQL may fail due to SQLite limitations
|
|
22
|
+
* - Use drizzle-kit CLI for complex migrations
|
|
23
|
+
* - Production:
|
|
24
|
+
* - PostgreSQL supports complex multi-statement migrations
|
|
25
|
+
* - Migrations are applied atomically with proper transaction handling
|
|
26
|
+
* - Migration journal tracks all applied changes for rollback capability
|
|
27
|
+
*
|
|
28
|
+
* Migration Folder Structure:
|
|
29
|
+
* - Located at: `{project_root}/drizzle/migrations`
|
|
30
|
+
* - Contains SQL migration files and a meta journal file
|
|
31
|
+
* - Migration files are named with timestamp prefix for ordering
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
|
35
|
+
import { db } from './index'
|
|
36
|
+
import path from 'path'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runs all pending database migrations
|
|
40
|
+
*
|
|
41
|
+
* This function checks for unapplied migrations in the migrations folder
|
|
42
|
+
* and applies them to the database in order. It uses Drizzle ORM's migration
|
|
43
|
+
* system which tracks applied migrations in a __drizzle_migrations table.
|
|
44
|
+
*
|
|
45
|
+
* Purpose:
|
|
46
|
+
* - Initialize the database schema in a new environment
|
|
47
|
+
* - Apply schema changes when upgrading the application
|
|
48
|
+
* - Ensure database structure matches the code schema
|
|
49
|
+
*
|
|
50
|
+
* Parameters:
|
|
51
|
+
* - None (uses default migrations folder path)
|
|
52
|
+
*
|
|
53
|
+
* Return Value:
|
|
54
|
+
* - Promise<void>: Resolves when all migrations complete successfully
|
|
55
|
+
*
|
|
56
|
+
* Side Effects:
|
|
57
|
+
* - Creates or modifies database tables and indexes
|
|
58
|
+
* - Inserts migration records into __drizzle_migrations table
|
|
59
|
+
* - Logs migration progress to console
|
|
60
|
+
* - May throw errors for failed migrations
|
|
61
|
+
*
|
|
62
|
+
* Error Handling:
|
|
63
|
+
* - Catches and re-throws migration errors with context
|
|
64
|
+
* - Special handling for multi-statement SQL errors in SQLite
|
|
65
|
+
* - Recommends alternative migration approach when push fails
|
|
66
|
+
*
|
|
67
|
+
* Example Usage:
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { runMigrations } from './db/migrate'
|
|
70
|
+
*
|
|
71
|
+
* async function initializeApp() {
|
|
72
|
+
* try {
|
|
73
|
+
* await runMigrations()
|
|
74
|
+
* console.log('Database is up to date')
|
|
75
|
+
* } catch (error) {
|
|
76
|
+
* console.error('Migration failed:', error)
|
|
77
|
+
* process.exit(1)
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* initializeApp()
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* CLI Usage:
|
|
85
|
+
* ```bash
|
|
86
|
+
* # Run migrations via npm script
|
|
87
|
+
* pnpm db:migrate
|
|
88
|
+
*
|
|
89
|
+
* # Or run directly with Node
|
|
90
|
+
* node -r dotenv/config dist/db/migrate.js
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Notes:
|
|
94
|
+
* - Migrations run in a transaction (atomic all-or-nothing)
|
|
95
|
+
* - Already-applied migrations are automatically skipped
|
|
96
|
+
* - Migration folder path is relative to process.cwd()
|
|
97
|
+
* - For SQLite, multi-statement migrations require drizzle-kit CLI
|
|
98
|
+
*
|
|
99
|
+
* @throws {Error} If migrations folder is not found
|
|
100
|
+
* @throws {Error} If a migration SQL file is invalid
|
|
101
|
+
* @throws {Error} If database connection fails
|
|
102
|
+
* @throws {Error} For multi-statement SQL in SQLite (with helpful message)
|
|
103
|
+
*/
|
|
104
|
+
export async function runMigrations() {
|
|
105
|
+
const migrationsFolder = path.join(process.cwd(), 'drizzle/migrations')
|
|
106
|
+
|
|
107
|
+
console.log('Running database migrations...')
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await migrate(db as any, { migrationsFolder })
|
|
111
|
+
console.log('✓ Migrations completed successfully')
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (
|
|
114
|
+
error instanceof Error &&
|
|
115
|
+
error.message.includes('more than one statement')
|
|
116
|
+
) {
|
|
117
|
+
console.error(
|
|
118
|
+
'⚠ Migration encountered multi-statement SQL. Please use drizzle-kit migrate command.'
|
|
119
|
+
)
|
|
120
|
+
console.error('Run: pnpm db:migrate')
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Schema Module
|
|
3
|
+
*
|
|
4
|
+
* This module defines the complete database schema for the application using Drizzle ORM.
|
|
5
|
+
* It uses SQLite as the default database, with PostgreSQL support for production environments.
|
|
6
|
+
*
|
|
7
|
+
* Database Design Decisions:
|
|
8
|
+
* - Uses cuid2 for generating unique, non-sequential IDs that are more secure than auto-increment
|
|
9
|
+
* - Stores timestamps as integers (Unix milliseconds) for SQLite compatibility
|
|
10
|
+
* - Implements cascade deletion to maintain referential integrity
|
|
11
|
+
* - Follows authentication best practices with separate accounts, sessions, and verification tables
|
|
12
|
+
*
|
|
13
|
+
* Migration Strategy:
|
|
14
|
+
* - Schema changes should be made using drizzle-kit generate to create migration files
|
|
15
|
+
* - Use `pnpm db:migrate` for complex migrations with multiple SQL statements
|
|
16
|
+
* - Use `npx drizzle-kit push` for simple schema changes during development
|
|
17
|
+
*
|
|
18
|
+
* Development vs Production:
|
|
19
|
+
* - Development: SQLite with WAL mode for better concurrency
|
|
20
|
+
* - Production: PostgreSQL for better scaling and reliability
|
|
21
|
+
* - All tables use integer timestamp mode which works seamlessly with both databases
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
25
|
+
import { createId } from '@paralleldrive/cuid2'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Users table
|
|
29
|
+
*
|
|
30
|
+
* Core user identity and profile information. This table stores the primary user
|
|
31
|
+
* record and serves as the parent for all related authentication and session data.
|
|
32
|
+
*
|
|
33
|
+
* Relationships:
|
|
34
|
+
* - One-to-many with sessions: A user can have multiple active sessions
|
|
35
|
+
* - One-to-many with accounts: A user can link multiple OAuth providers
|
|
36
|
+
*
|
|
37
|
+
* Key Constraints:
|
|
38
|
+
* - email: Unique constraint ensures no duplicate email addresses
|
|
39
|
+
* - emailVerified: Defaults to false, requires explicit verification
|
|
40
|
+
*
|
|
41
|
+
* Indexes:
|
|
42
|
+
* - Primary key: id (cuid2 string)
|
|
43
|
+
* - Unique index: email
|
|
44
|
+
*
|
|
45
|
+
* Cascade Behavior:
|
|
46
|
+
* - Deleting a user cascades to related sessions and accounts via ON DELETE CASCADE
|
|
47
|
+
*/
|
|
48
|
+
export const users = sqliteTable('users', {
|
|
49
|
+
id: text('id')
|
|
50
|
+
.primaryKey()
|
|
51
|
+
.$defaultFn(() => createId()),
|
|
52
|
+
email: text('email').unique().notNull(),
|
|
53
|
+
name: text('name').notNull(),
|
|
54
|
+
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false),
|
|
55
|
+
image: text('image'),
|
|
56
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(
|
|
57
|
+
() => new Date()
|
|
58
|
+
),
|
|
59
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).$onUpdateFn(
|
|
60
|
+
() => new Date()
|
|
61
|
+
),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sessions table
|
|
66
|
+
*
|
|
67
|
+
* Manages active user sessions for authentication. Each session represents a logged-in
|
|
68
|
+
* user across a specific device or browser.
|
|
69
|
+
*
|
|
70
|
+
* Relationships:
|
|
71
|
+
* - Many-to-one with users: Each session belongs to exactly one user
|
|
72
|
+
*
|
|
73
|
+
* Key Constraints:
|
|
74
|
+
* - userId: Foreign key to users table with cascade delete
|
|
75
|
+
* - expiresAt: Required timestamp for session expiration
|
|
76
|
+
*
|
|
77
|
+
* Indexes:
|
|
78
|
+
* - Primary key: id (session token)
|
|
79
|
+
* - Foreign key index: user_id for efficient lookups
|
|
80
|
+
*
|
|
81
|
+
* Cascade Behavior:
|
|
82
|
+
* - Deleting the associated user automatically removes all sessions
|
|
83
|
+
*/
|
|
84
|
+
export const sessions = sqliteTable('sessions', {
|
|
85
|
+
id: text('id').primaryKey(),
|
|
86
|
+
userId: text('user_id')
|
|
87
|
+
.notNull()
|
|
88
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
89
|
+
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
|
|
90
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(
|
|
91
|
+
() => new Date()
|
|
92
|
+
),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Accounts table
|
|
97
|
+
*
|
|
98
|
+
* Stores OAuth provider connections for each user. Supports linking multiple
|
|
99
|
+
* authentication providers (Google, GitHub, etc.) to a single user account.
|
|
100
|
+
*
|
|
101
|
+
* Relationships:
|
|
102
|
+
* - Many-to-one with users: Multiple accounts can belong to one user
|
|
103
|
+
*
|
|
104
|
+
* Key Constraints:
|
|
105
|
+
* - userId: Foreign key to users table with cascade delete
|
|
106
|
+
* - accountId: Unique identifier from the OAuth provider
|
|
107
|
+
* - providerId: OAuth provider name (e.g., 'google', 'github')
|
|
108
|
+
*
|
|
109
|
+
* Indexes:
|
|
110
|
+
* - Primary key: id (cuid2 string)
|
|
111
|
+
* - Foreign key index: user_id
|
|
112
|
+
* - Composite unique: (account_id, provider_id) prevents duplicate provider links
|
|
113
|
+
*
|
|
114
|
+
* Cascade Behavior:
|
|
115
|
+
* - Deleting the associated user removes all OAuth provider connections
|
|
116
|
+
*/
|
|
117
|
+
export const accounts = sqliteTable('accounts', {
|
|
118
|
+
id: text('id')
|
|
119
|
+
.primaryKey()
|
|
120
|
+
.$defaultFn(() => createId()),
|
|
121
|
+
userId: text('user_id')
|
|
122
|
+
.notNull()
|
|
123
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
124
|
+
accountId: text('account_id').notNull(),
|
|
125
|
+
providerId: text('provider_id').notNull(),
|
|
126
|
+
accessToken: text('access_token'),
|
|
127
|
+
refreshToken: text('refresh_token'),
|
|
128
|
+
expiresAt: integer('expires_at', { mode: 'timestamp' }),
|
|
129
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(
|
|
130
|
+
() => new Date()
|
|
131
|
+
),
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Verifications table
|
|
136
|
+
*
|
|
137
|
+
* Stores temporary verification codes for email verification, password reset,
|
|
138
|
+
* and other one-time authentication flows.
|
|
139
|
+
*
|
|
140
|
+
* Relationships:
|
|
141
|
+
* - No foreign key relationships - standalone table
|
|
142
|
+
*
|
|
143
|
+
* Key Constraints:
|
|
144
|
+
* - identifier: Email address or phone number being verified
|
|
145
|
+
* - value: Verification code or token
|
|
146
|
+
* - expiresAt: Required timestamp for code expiration
|
|
147
|
+
*
|
|
148
|
+
* Indexes:
|
|
149
|
+
* - Primary key: id (cuid2 string)
|
|
150
|
+
* - Non-unique index: identifier for efficient lookup by email/phone
|
|
151
|
+
*
|
|
152
|
+
* Cascade Behavior:
|
|
153
|
+
* - No cascade behavior - records must be manually expired or deleted
|
|
154
|
+
*
|
|
155
|
+
* Usage:
|
|
156
|
+
* - Records are typically created with a short expiry (e.g., 15-30 minutes)
|
|
157
|
+
* - After successful verification, records should be deleted to prevent reuse
|
|
158
|
+
* - Expired records can be cleaned up via a scheduled job
|
|
159
|
+
*/
|
|
160
|
+
export const verifications = sqliteTable('verifications', {
|
|
161
|
+
id: text('id')
|
|
162
|
+
.primaryKey()
|
|
163
|
+
.$defaultFn(() => createId()),
|
|
164
|
+
identifier: text('identifier').notNull(),
|
|
165
|
+
value: text('value').notNull(),
|
|
166
|
+
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
|
|
167
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(
|
|
168
|
+
() => new Date()
|
|
169
|
+
),
|
|
170
|
+
})
|