@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.
Files changed (64) hide show
  1. package/bin/index.js +2 -0
  2. package/dist/index.js +592 -0
  3. package/package.json +43 -0
  4. package/templates/default/.editorconfig +21 -0
  5. package/templates/default/.env.example +15 -0
  6. package/templates/default/.eslintrc.json +35 -0
  7. package/templates/default/.prettierrc.json +7 -0
  8. package/templates/default/README.md +346 -0
  9. package/templates/default/app.config.ts +20 -0
  10. package/templates/default/docs/adding-features.md +439 -0
  11. package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
  12. package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
  13. package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
  14. package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
  15. package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
  16. package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
  17. package/templates/default/docs/architecture.md +241 -0
  18. package/templates/default/docs/database.md +376 -0
  19. package/templates/default/docs/deployment.md +435 -0
  20. package/templates/default/docs/troubleshooting.md +668 -0
  21. package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
  22. package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
  23. package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
  24. package/templates/default/drizzle.config.ts +10 -0
  25. package/templates/default/lighthouserc.json +78 -0
  26. package/templates/default/src/app/__root.tsx +32 -0
  27. package/templates/default/src/app/api/auth/$.ts +15 -0
  28. package/templates/default/src/app/api/trpc.server.ts +12 -0
  29. package/templates/default/src/app/auth/forgot-password.tsx +107 -0
  30. package/templates/default/src/app/auth/login.tsx +34 -0
  31. package/templates/default/src/app/auth/register.tsx +34 -0
  32. package/templates/default/src/app/auth/reset-password.tsx +171 -0
  33. package/templates/default/src/app/auth/verify-email.tsx +111 -0
  34. package/templates/default/src/app/dashboard/index.tsx +122 -0
  35. package/templates/default/src/app/dashboard/settings.tsx +161 -0
  36. package/templates/default/src/app/globals.css +55 -0
  37. package/templates/default/src/app/index.tsx +83 -0
  38. package/templates/default/src/components/features/auth/login-form.tsx +172 -0
  39. package/templates/default/src/components/features/auth/register-form.tsx +202 -0
  40. package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
  41. package/templates/default/src/components/layout/header.tsx +29 -0
  42. package/templates/default/src/components/layout/sidebar.tsx +38 -0
  43. package/templates/default/src/components/ui/button.tsx +57 -0
  44. package/templates/default/src/components/ui/card.tsx +79 -0
  45. package/templates/default/src/components/ui/input.tsx +24 -0
  46. package/templates/default/src/lib/api.ts +42 -0
  47. package/templates/default/src/lib/auth.ts +292 -0
  48. package/templates/default/src/lib/email.ts +221 -0
  49. package/templates/default/src/lib/env.ts +119 -0
  50. package/templates/default/src/lib/hydration-timing.ts +289 -0
  51. package/templates/default/src/lib/monitoring.ts +336 -0
  52. package/templates/default/src/lib/utils.ts +6 -0
  53. package/templates/default/src/server/api/root.ts +10 -0
  54. package/templates/default/src/server/api/routers/dashboard.ts +37 -0
  55. package/templates/default/src/server/api/routers/user.ts +31 -0
  56. package/templates/default/src/server/api/trpc.ts +132 -0
  57. package/templates/default/src/server/auth/config.ts +241 -0
  58. package/templates/default/src/server/db/index.ts +153 -0
  59. package/templates/default/src/server/db/migrate.ts +125 -0
  60. package/templates/default/src/server/db/schema.ts +170 -0
  61. package/templates/default/src/server/db/seed.ts +130 -0
  62. package/templates/default/src/types/global.d.ts +25 -0
  63. package/templates/default/tailwind.config.js +46 -0
  64. 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
+ })