@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,336 @@
1
+ /**
2
+ * Performance monitoring and query tracking module.
3
+ *
4
+ * This module provides tools to track database query performance and identify
5
+ * slow operations that may impact application responsiveness. It collects timing
6
+ * data for queries and provides methods to analyze performance patterns.
7
+ *
8
+ * Key patterns and conventions:
9
+ * - Singleton pattern: A single performanceMonitor instance is shared across
10
+ * the application to aggregate metrics
11
+ * - Query keys are truncated to 100 characters to reduce memory usage while
12
+ * preserving enough context to identify the query
13
+ * - Slow queries are logged to console in development mode only
14
+ * - Metrics are stored in-memory and lost on process restart (by design for simplicity)
15
+ * - Average duration is used instead of individual samples to identify patterns
16
+ *
17
+ * Performance implications:
18
+ * - Minimal overhead: timing measurements use performance.now() which is
19
+ * sub-microsecond precision and has negligible overhead
20
+ * - Memory usage grows with the number of unique queries but is bounded
21
+ * by query key truncation (100 chars per key)
22
+ * - No disk I/O: All metrics are kept in memory to avoid affecting query performance
23
+ * - Automatic collection: When wrapping queries with measureQuery/measureSyncQuery,
24
+ * timing happens automatically without manual instrumentation
25
+ *
26
+ * Configuration decisions:
27
+ * - SLOW_QUERY_THRESHOLD of 1000ms (1 second) is chosen because:
28
+ * - Web requests should ideally complete within 100ms-200ms
29
+ * - Database queries over 1 second are noticeably slow and worth investigating
30
+ * - Threshold is configurable via getSlowQueries(threshold) parameter
31
+ * - 100 character query key truncation balances:
32
+ * - Memory efficiency (shorter keys = less memory)
33
+ * - Debugging utility (enough context to identify the query)
34
+ * - Development-only logging avoids performance impact in production
35
+ * - No persistence: In-memory storage is sufficient for development debugging
36
+ *
37
+ * Usage patterns:
38
+ * - Wrap database queries with measureQuery for async operations
39
+ * - Wrap synchronous operations with measureSyncQuery
40
+ * - Call getSlowQueries() periodically to analyze performance
41
+ * - Call reset() to clear metrics (e.g., between test runs)
42
+ *
43
+ * @example
44
+ * // Wrap an async database query
45
+ * const users = await measureQuery(
46
+ * 'SELECT * FROM users',
47
+ * () => db.query.users.findMany()
48
+ * )
49
+ *
50
+ * @example
51
+ * // Get slow queries for analysis
52
+ * const slowQueries = performanceMonitor.getSlowQueries()
53
+ * slowQueries.forEach(({ query, avgDuration, count }) => {
54
+ * console.log(`${query}: ${avgDuration}ms avg (${count} executions)`)
55
+ * })
56
+ */
57
+
58
+ import { env } from './env'
59
+
60
+ /**
61
+ * Threshold in milliseconds for considering a query "slow".
62
+ *
63
+ * Any query taking longer than this duration will be logged in development
64
+ * and included in getSlowQueries() results.
65
+ *
66
+ * Rationale: 1000ms (1 second) is chosen because:
67
+ * - User-perceivable delay starts at around 100ms
68
+ * - Database queries over 1 second indicate potential optimization opportunities
69
+ * - Allows catching significant performance issues while filtering out
70
+ * minor variations that don't impact user experience
71
+ */
72
+ const SLOW_QUERY_THRESHOLD = 1000 // 1 second in milliseconds
73
+
74
+ /**
75
+ * Performance monitoring class for tracking query execution times.
76
+ *
77
+ * This class maintains a registry of query execution times and provides
78
+ * methods to record timings and analyze slow queries. Use the singleton
79
+ * instance `performanceMonitor` instead of instantiating this class directly.
80
+ *
81
+ * Thread safety: This class is not thread-safe but JavaScript is single-threaded,
82
+ * so this is not a concern in the Node.js runtime.
83
+ *
84
+ * Memory usage: Grows linearly with the number of unique queries. Each unique
85
+ * query stores an array of duration samples. For production use with many
86
+ * unique queries, consider implementing a periodic cleanup or sampling strategy.
87
+ */
88
+ export class PerformanceMonitor {
89
+ /**
90
+ * Map storing query execution times.
91
+ *
92
+ * Key: First 100 characters of the query string
93
+ * Value: Array of execution durations in milliseconds
94
+ *
95
+ * Truncating keys to 100 characters reduces memory usage while preserving
96
+ * enough context to identify the query. Multiple variations of the same query
97
+ * (e.g., different WHERE clause values) will be aggregated under the same key,
98
+ * which is desirable for identifying patterns rather than individual executions.
99
+ */
100
+ private queryTimes = new Map<string, number[]>()
101
+
102
+ /**
103
+ * Records the execution time of a query.
104
+ *
105
+ * This method should be called immediately after a query completes with the
106
+ * actual duration measured. If the query exceeds the slow threshold, a warning
107
+ * will be logged in development mode.
108
+ *
109
+ Performance characteristics:
110
+ * - Time complexity: O(1) for Map insertion
111
+ * - Space complexity: O(n) where n is the number of unique queries
112
+ * - Negligible overhead (sub-microsecond for timing recording)
113
+ *
114
+ * Security implications:
115
+ * - Query strings may contain sensitive data (user inputs, etc.)
116
+ * - In production, avoid logging full queries if they contain PII
117
+ * - The truncated key (100 chars) reduces exposure risk
118
+ *
119
+ * @param query - The SQL query or operation description. Truncated to 100 chars.
120
+ * @param duration - Execution time in milliseconds. Should be measured using
121
+ * performance.now() for accuracy.
122
+ *
123
+ * @example
124
+ * const startTime = performance.now()
125
+ * await db.query.users.findMany()
126
+ * const duration = performance.now() - startTime
127
+ * performanceMonitor.recordQuery('SELECT * FROM users', duration)
128
+ */
129
+ recordQuery(query: string, duration: number) {
130
+ /**
131
+ * Truncate query to first 100 characters.
132
+ *
133
+ * This serves two purposes:
134
+ * 1. Memory efficiency: shorter keys use less memory
135
+ * 2. Pattern aggregation: similar queries with different parameters
136
+ * are grouped together for analysis
137
+ */
138
+ const queryKey = query.slice(0, 100)
139
+
140
+ if (!this.queryTimes.has(queryKey)) {
141
+ this.queryTimes.set(queryKey, [])
142
+ }
143
+
144
+ this.queryTimes.get(queryKey)!.push(duration)
145
+
146
+ /**
147
+ * Log slow queries in development mode.
148
+ *
149
+ * Development-only logging avoids:
150
+ * - Performance impact in production
151
+ * - Potential exposure of sensitive query data in production logs
152
+ * - Log noise in production where performance may be acceptable
153
+ */
154
+ if (duration > SLOW_QUERY_THRESHOLD) {
155
+ if (env.NODE_ENV === 'development') {
156
+ console.warn(`[Slow Query] ${duration}ms: ${queryKey}`)
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Retrieves all queries that exceed a given average duration threshold.
163
+ *
164
+ * This method calculates the average execution time for each query and returns
165
+ * those whose average exceeds the threshold. Results are sorted by average
166
+ duration in descending order (slowest first) for prioritization.
167
+ *
168
+ * Performance characteristics:
169
+ * - Time complexity: O(n * m) where n is the number of unique queries and m
170
+ * is the average number of samples per query
171
+ * - Typically very fast unless thousands of queries have been recorded
172
+ * - Sorting adds O(n log n) overhead
173
+ *
174
+ * Use cases:
175
+ * - Identifying optimization opportunities during development
176
+ * - Generating performance reports
177
+ * - Debugging production performance issues (via logging)
178
+ *
179
+ * @param threshold - Average duration threshold in milliseconds. Defaults to
180
+ * SLOW_QUERY_THRESHOLD (1000ms). Queries with average
181
+ * duration below this are filtered out.
182
+ * @returns Array of slow query metrics, sorted by average duration descending.
183
+ * Each entry contains the query (truncated), average duration (rounded),
184
+ * and execution count.
185
+ *
186
+ * @example
187
+ * const slowQueries = performanceMonitor.getSlowQueries(500)
188
+ * slowQueries.forEach(({ query, avgDuration, count }) => {
189
+ * console.log(`${query}: ${avgDuration}ms average over ${count} executions`)
190
+ * })
191
+ */
192
+ getSlowQueries(
193
+ threshold = SLOW_QUERY_THRESHOLD
194
+ ): Array<{ query: string; avgDuration: number; count: number }> {
195
+ const results: Array<{
196
+ query: string
197
+ avgDuration: number
198
+ count: number
199
+ }> = []
200
+
201
+ this.queryTimes.forEach((durations, query) => {
202
+ const avgDuration =
203
+ durations.reduce((a, b) => a + b, 0) / durations.length
204
+
205
+ if (avgDuration > threshold) {
206
+ results.push({
207
+ query,
208
+ avgDuration: Math.round(avgDuration),
209
+ count: durations.length,
210
+ })
211
+ }
212
+ })
213
+
214
+ /**
215
+ * Sort by average duration descending.
216
+ *
217
+ * This puts the slowest queries first, which is useful for prioritizing
218
+ * optimization efforts.
219
+ */
220
+ return results.sort((a, b) => b.avgDuration - a.avgDuration)
221
+ }
222
+
223
+ /**
224
+ * Clears all recorded query metrics.
225
+ *
226
+ * This method is useful for:
227
+ * - Starting fresh measurements after code changes
228
+ * - Clearing metrics between test runs
229
+ * - Resetting state in development
230
+ *
231
+ * Performance characteristics:
232
+ * - Time complexity: O(n) where n is the number of unique queries
233
+ * - Memory is immediately released (in V8, subject to garbage collection)
234
+ *
235
+ * @example
236
+ * // Clear metrics before running a performance test
237
+ * performanceMonitor.reset()
238
+ * await runPerformanceTest()
239
+ * const results = performanceMonitor.getSlowQueries()
240
+ */
241
+ reset() {
242
+ this.queryTimes.clear()
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Singleton instance of PerformanceMonitor.
248
+ *
249
+ * Use this instance throughout the application to aggregate metrics from
250
+ * all parts of the codebase. Having a single instance allows for comprehensive
251
+ * performance analysis across all database queries and operations.
252
+ *
253
+ * @example
254
+ * import { performanceMonitor } from '@/lib/monitoring'
255
+ * performanceMonitor.recordQuery('SELECT * FROM users', 125)
256
+ */
257
+ export const performanceMonitor = new PerformanceMonitor()
258
+
259
+ /**
260
+ * Wraps an async function with performance measurement.
261
+ *
262
+ * This utility automatically measures the execution time of an async function
263
+ * and records it with the performance monitor. Use this for database queries,
264
+ * API calls, or any async operation you want to track.
265
+ *
266
+ * Performance characteristics:
267
+ * - Overhead: ~0.1ms for timing (performance.now() calls)
268
+ * - Async/await overhead: minimal (<1ms) for Promise wrapping
269
+ * - Memory: No additional memory allocation per call
270
+ *
271
+ * Error handling: Errors are propagated unchanged. The timing measurement
272
+ * includes the time taken by the function, including any error handling.
273
+ *
274
+ * @param query - Description of the operation (e.g., SQL query or function name).
275
+ * Used as the key in the performance monitor.
276
+ * @param fn - The async function to measure. Should return a Promise.
277
+ * @returns Promise that resolves to the result of fn(), with timing recorded.
278
+ *
279
+ * @example
280
+ * const users = await measureQuery(
281
+ * 'SELECT * FROM users WHERE active = true',
282
+ * () => db.query.users.findMany({ where: { active: true } })
283
+ * )
284
+ */
285
+ export function measureQuery<T>(
286
+ query: string,
287
+ fn: () => Promise<T>
288
+ ): Promise<T> {
289
+ return new Promise(async (resolve, reject) => {
290
+ const startTime = performance.now()
291
+ try {
292
+ const result = await fn()
293
+ const duration = performance.now() - startTime
294
+ performanceMonitor.recordQuery(query, duration)
295
+ resolve(result)
296
+ } catch (error) {
297
+ reject(error)
298
+ }
299
+ })
300
+ }
301
+
302
+ /**
303
+ * Wraps a synchronous function with performance measurement.
304
+ *
305
+ * This utility automatically measures the execution time of a synchronous function
306
+ * and records it with the performance monitor. Use this for CPU-bound operations,
307
+ * synchronous I/O, or any synchronous code you want to track.
308
+ *
309
+ * Performance characteristics:
310
+ * - Overhead: ~0.1ms for timing (performance.now() calls)
311
+ * - Memory: No additional memory allocation per call
312
+ * - Suitable for operations taking >1ms (otherwise overhead is significant)
313
+ *
314
+ * Use cases:
315
+ * - Tracking synchronous database operations (e.g., SQLite queries)
316
+ * - Measuring data transformation time
317
+ * - Profiling CPU-intensive computations
318
+ *
319
+ * @param query - Description of the operation (e.g., function name or operation).
320
+ * Used as the key in the performance monitor.
321
+ * @param fn - The synchronous function to measure.
322
+ * @returns The result of fn(), with timing recorded.
323
+ *
324
+ * @example
325
+ * const users = measureSyncQuery(
326
+ * 'db.query.users.findMany()',
327
+ * () => db.query.users.findMany()
328
+ * )
329
+ */
330
+ export function measureSyncQuery<T>(query: string, fn: () => T): T {
331
+ const startTime = performance.now()
332
+ const result = fn()
333
+ const duration = performance.now() - startTime
334
+ performanceMonitor.recordQuery(query, duration)
335
+ return result
336
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,10 @@
1
+ import { router } from './trpc'
2
+ import { userRouter } from './routers/user'
3
+ import { dashboardRouter } from './routers/dashboard'
4
+
5
+ export const appRouter = router({
6
+ user: userRouter,
7
+ dashboard: dashboardRouter,
8
+ })
9
+
10
+ export type AppRouter = typeof appRouter
@@ -0,0 +1,37 @@
1
+ import { router, protectedProcedure } from '../trpc'
2
+ import { db } from '@/server/db'
3
+ import { users } from '@/server/db/schema'
4
+ import { eq, and, gte, sql } from 'drizzle-orm'
5
+
6
+ export const dashboardRouter = router({
7
+ getStats: protectedProcedure.query(async ({ ctx }) => {
8
+ const [totalUsersResult] = await db
9
+ .select({ count: sql<number>`count(*)` })
10
+ .from(users)
11
+
12
+ const [activeUsersResult] = await db
13
+ .select({ count: sql<number>`count(*)` })
14
+ .from(users)
15
+ .where(
16
+ and(
17
+ gte(users.createdAt, new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
18
+ )
19
+ )
20
+
21
+ return {
22
+ totalUsers: Number(totalUsersResult?.count ?? 0),
23
+ activeUsers: Number(activeUsersResult?.count ?? 0),
24
+ currentUser: ctx.user,
25
+ }
26
+ }),
27
+
28
+ getUserProfile: protectedProcedure.query(async ({ ctx }) => {
29
+ const [user] = await db
30
+ .select()
31
+ .from(users)
32
+ .where(eq(users.id, ctx.user.id))
33
+ .limit(1)
34
+
35
+ return user ?? null
36
+ }),
37
+ })
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod'
2
+ import { router, protectedProcedure } from '../trpc'
3
+ import { db } from '@/server/db'
4
+ import { users } from '@/server/db/schema'
5
+ import { eq } from 'drizzle-orm'
6
+
7
+ export const userRouter = router({
8
+ getProfile: protectedProcedure.query(async ({ ctx }) => {
9
+ const result = await (db
10
+ .select()
11
+ .from(users)
12
+ .where(eq(users.id, ctx.user.id))
13
+ .limit(1) as any)
14
+ return result[0] ?? null
15
+ }),
16
+
17
+ updateProfile: protectedProcedure
18
+ .input(
19
+ z.object({
20
+ name: z.string().min(1).max(100),
21
+ })
22
+ )
23
+ .mutation(async ({ ctx, input }) => {
24
+ const result = await (db
25
+ .update(users)
26
+ .set({ name: input.name, updatedAt: new Date() })
27
+ .where(eq(users.id, ctx.user.id))
28
+ .returning() as any)
29
+ return result[0]
30
+ }),
31
+ })
@@ -0,0 +1,132 @@
1
+ /**
2
+ * tRPC configuration and context setup
3
+ *
4
+ * This file sets up the tRPC server with:
5
+ * - Request context with database connection and authentication
6
+ * - Middleware for auth checks
7
+ * - Base procedures (public, protected) for different access levels
8
+ *
9
+ * @example
10
+ * // Create a protected procedure
11
+ * export const userRouter = router({
12
+ * getProfile: protectedProcedure.query(({ ctx }) => {
13
+ * return ctx.user
14
+ * })
15
+ * })
16
+ */
17
+
18
+ import { initTRPC, TRPCError } from '@trpc/server'
19
+ import { auth } from '@/server/auth/config'
20
+ import { type FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
21
+
22
+ /**
23
+ * Creates the tRPC context for each request
24
+ *
25
+ * This function is called for every tRPC request to create a context object
26
+ * that is available to all procedures. The context includes:
27
+ * - User session data from the authentication system
28
+ * - User information if authenticated, null otherwise
29
+ *
30
+ * Architectural decision: Context is created per-request rather than using
31
+ * a singleton because authentication state varies per request. Using async
32
+ * context creation allows us to fetch session data from cookies/headers.
33
+ *
34
+ * @param opts - Fetch adapter options containing the request object
35
+ * @param opts.req - The incoming HTTP request with headers
36
+ * @returns Context object with user and session data
37
+ *
38
+ * @example
39
+ * // The context is automatically used in procedures
40
+ * const procedure = publicProcedure.query(({ ctx }) => {
41
+ * console.log(ctx.user) // User object or null
42
+ * console.log(ctx.session) // Session object or null
43
+ * })
44
+ */
45
+ export const createTRPCContext = async (opts: FetchCreateContextFnOptions) => {
46
+ const session = await auth.api.getSession({
47
+ headers: opts.req.headers,
48
+ })
49
+
50
+ return {
51
+ user: session?.user ?? null,
52
+ session: session?.session ?? null,
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Initialized tRPC instance with context typing
58
+ *
59
+ * Architectural decision: We use initTRPC with .context() to create a
60
+ * strongly-typed tRPC instance. This ensures type safety across all routers
61
+ * and procedures, providing autocomplete and compile-time checks for the
62
+ * context object structure.
63
+ */
64
+ const t = initTRPC.context<typeof createTRPCContext>().create()
65
+
66
+ /**
67
+ * Router factory function
68
+ *
69
+ * Creates a new tRPC router by combining multiple procedures and sub-routers.
70
+ * Use this to organize API endpoints into logical groups.
71
+ *
72
+ * @example
73
+ * export const appRouter = router({
74
+ * greeting: publicProcedure.query(() => 'Hello World'),
75
+ * users: userRouter,
76
+ * })
77
+ */
78
+ export const router = t.router
79
+
80
+ /**
81
+ * Public procedure factory
82
+ *
83
+ * Creates a procedure that can be accessed without authentication.
84
+ * Use this for endpoints that don't require user login.
85
+ *
86
+ * Architectural decision: Having explicit public and protected procedures
87
+ * makes authentication requirements clear and prevents accidental exposure
88
+ * of protected data. The default is public to reduce friction for new endpoints.
89
+ *
90
+ * @example
91
+ * export const healthRouter = router({
92
+ * check: publicProcedure.query(() => ({ status: 'ok' }))
93
+ * })
94
+ */
95
+ export const publicProcedure = t.procedure
96
+
97
+ /**
98
+ * Protected procedure with authentication middleware
99
+ *
100
+ * Creates a procedure that requires user authentication. The middleware
101
+ * checks if a user is present in the context and throws an UNAUTHORIZED
102
+ * error if not.
103
+ *
104
+ * Architectural decision: This middleware pattern centralizes auth logic
105
+ * rather than checking authentication in each procedure. It also narrows
106
+ * the context type to guarantee that ctx.user and ctx.session are non-null,
107
+ * providing type safety in protected procedures.
108
+ *
109
+ * @throws {TRPCError} UNAUTHORIZED if user is not authenticated
110
+ *
111
+ * @example
112
+ * export const userRouter = router({
113
+ * getProfile: protectedProcedure.query(({ ctx }) => {
114
+ * // ctx.user and ctx.session are guaranteed to be non-null here
115
+ * return ctx.user
116
+ * })
117
+ * })
118
+ */
119
+ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
120
+ if (!ctx.user) {
121
+ throw new TRPCError({
122
+ code: 'UNAUTHORIZED',
123
+ message: 'You must be logged in to access this resource',
124
+ })
125
+ }
126
+ return next({
127
+ ctx: {
128
+ user: ctx.user,
129
+ session: ctx.session!,
130
+ },
131
+ })
132
+ })