codecruise 0.1.0

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/bin/codecruise.js +68 -0
  4. package/config/CLAUDE.md +107 -0
  5. package/config/agents/analyst.md +48 -0
  6. package/config/agents/architect-reviewer.md +161 -0
  7. package/config/agents/architect.md +119 -0
  8. package/config/agents/critic.md +63 -0
  9. package/config/agents/developer.md +96 -0
  10. package/config/agents/devops.md +81 -0
  11. package/config/agents/orchestrator.md +91 -0
  12. package/config/agents/planner.md +139 -0
  13. package/config/agents/retro.md +52 -0
  14. package/config/agents/reviewer.md +101 -0
  15. package/config/agents/security-reviewer.md +57 -0
  16. package/config/agents/stack/expo/AGENT.md +473 -0
  17. package/config/agents/stack/expo/rules/critical.md +427 -0
  18. package/config/agents/stack/expo/rules/native.md +455 -0
  19. package/config/agents/stack/expo/rules/navigation.md +445 -0
  20. package/config/agents/stack/expo/rules/performance.md +415 -0
  21. package/config/agents/stack/fastify/AGENT.md +397 -0
  22. package/config/agents/stack/fastify/rules/api-design.md +283 -0
  23. package/config/agents/stack/fastify/rules/critical.md +232 -0
  24. package/config/agents/stack/fastify/rules/queues.md +303 -0
  25. package/config/agents/stack/fastify/rules/security.md +384 -0
  26. package/config/agents/stack/index.yaml +48 -0
  27. package/config/agents/stack/nextjs/AGENT.md +421 -0
  28. package/config/agents/stack/nextjs/rules/components.md +413 -0
  29. package/config/agents/stack/nextjs/rules/critical.md +391 -0
  30. package/config/agents/stack/nextjs/rules/performance.md +403 -0
  31. package/config/agents/stack/nextjs/rules/styling.md +334 -0
  32. package/config/agents/stack/shared-ts/AGENT.md +384 -0
  33. package/config/agents/stack/shared-ts/rules/critical.md +315 -0
  34. package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
  35. package/config/agents/stack/shared-ts/rules/zod.md +427 -0
  36. package/config/agents/tester.md +79 -0
  37. package/config/commands/architect-discuss.md +366 -0
  38. package/config/commands/architect-list.md +160 -0
  39. package/config/commands/architect-review.md +111 -0
  40. package/config/commands/architect.md +118 -0
  41. package/config/commands/compact.md +118 -0
  42. package/config/commands/companion.md +279 -0
  43. package/config/commands/dashboard.md +152 -0
  44. package/config/commands/doctor.md +227 -0
  45. package/config/commands/dogfood-report.md +101 -0
  46. package/config/commands/flags/run-autonomous.md +110 -0
  47. package/config/commands/flags/run-pause.md +80 -0
  48. package/config/commands/ingest.md +173 -0
  49. package/config/commands/init.md +128 -0
  50. package/config/commands/metrics.md +87 -0
  51. package/config/commands/parallel.md +320 -0
  52. package/config/commands/pause.md +55 -0
  53. package/config/commands/plan-review.md +130 -0
  54. package/config/commands/plan.md +216 -0
  55. package/config/commands/production-check.md +308 -0
  56. package/config/commands/refine.md +323 -0
  57. package/config/commands/resume.md +72 -0
  58. package/config/commands/retro.md +121 -0
  59. package/config/commands/retry.md +75 -0
  60. package/config/commands/role.md +310 -0
  61. package/config/commands/run.md +417 -0
  62. package/config/commands/scope.md +85 -0
  63. package/config/commands/setup-permissions.md +104 -0
  64. package/config/commands/skip.md +75 -0
  65. package/config/commands/spec-forge.md +213 -0
  66. package/config/commands/spec-help.md +194 -0
  67. package/config/commands/spec-patch.md +342 -0
  68. package/config/commands/spec-resolve.md +110 -0
  69. package/config/commands/spec-review.md +153 -0
  70. package/config/commands/status.md +114 -0
  71. package/config/commands/sync.md +131 -0
  72. package/config/commands/task.md +138 -0
  73. package/config/commands/verify.md +124 -0
  74. package/config/hooks/README.md +632 -0
  75. package/config/hooks/activity-log.sh +187 -0
  76. package/config/hooks/anti-rationalize.sh +52 -0
  77. package/config/hooks/capture-verification.sh +112 -0
  78. package/config/hooks/collect-metrics.sh +135 -0
  79. package/config/hooks/enforce-file-scope.sh +75 -0
  80. package/config/hooks/enforce-state-machine.sh +161 -0
  81. package/config/hooks/enforce-tdd.sh +180 -0
  82. package/config/hooks/format.sh +40 -0
  83. package/config/hooks/lib/activity-helpers.sh +162 -0
  84. package/config/hooks/lib/read-settings.sh +71 -0
  85. package/config/hooks/load-context-skills.sh +95 -0
  86. package/config/hooks/notify.sh +81 -0
  87. package/config/hooks/pre-commit.sample +35 -0
  88. package/config/hooks/protect-files.sh +63 -0
  89. package/config/hooks/track-agents.sh +41 -0
  90. package/config/hooks/track-commands.sh +37 -0
  91. package/config/hooks/track-enforcement.sh +44 -0
  92. package/config/hooks/track-ooda.sh +77 -0
  93. package/config/hooks/validate-commit-msg.sh +35 -0
  94. package/config/hooks/validate-plan.sh +213 -0
  95. package/config/hooks/verify-criteria.sh +46 -0
  96. package/config/hooks/verify-todo-completion.sh +140 -0
  97. package/config/rules/comments.md +25 -0
  98. package/config/rules/decision-rules.md +308 -0
  99. package/config/rules/hygiene.md +247 -0
  100. package/config/rules/pattern-detection.md +372 -0
  101. package/config/rules/profiles.md +193 -0
  102. package/config/rules/recovery.md +83 -0
  103. package/config/rules/scope-detection.md +213 -0
  104. package/config/rules/standards.md +127 -0
  105. package/config/rules/workflow.md +121 -0
  106. package/config/schemas.md +767 -0
  107. package/config/settings.json +195 -0
  108. package/config/skills/backend/SKILL.md +734 -0
  109. package/config/skills/database/SKILL.md +426 -0
  110. package/config/skills/frontend/SKILL.md +434 -0
  111. package/config/skills/git/SKILL.md +396 -0
  112. package/config/skills/index.yaml +36 -0
  113. package/config/skills/observability/SKILL.md +430 -0
  114. package/config/skills/package-dev/SKILL.md +498 -0
  115. package/config/skills/performance/SKILL.md +378 -0
  116. package/config/skills/resilience/SKILL.md +573 -0
  117. package/config/skills/testing/SKILL.md +398 -0
  118. package/config/skills/testing-patterns/SKILL.md +276 -0
  119. package/config/skills/typescript/SKILL.md +152 -0
  120. package/config/templates/CLAUDE.md +70 -0
  121. package/config/templates/README.md +117 -0
  122. package/config/templates/steering/adr-template.md +102 -0
  123. package/config/templates/steering/product.md +60 -0
  124. package/config/templates/steering/rfc-template.md +159 -0
  125. package/config/templates/steering/structure.md +146 -0
  126. package/config/templates/steering/tech.md +85 -0
  127. package/package.json +40 -0
  128. package/src/install.js +163 -0
  129. package/src/report.js +310 -0
@@ -0,0 +1,734 @@
1
+ ---
2
+ name: backend
3
+ description: API design, database, and security patterns
4
+ version: 1.0.0
5
+ triggers:
6
+ - api
7
+ - endpoint
8
+ - database
9
+ - SQL
10
+ - auth
11
+ - server
12
+ - REST
13
+ - tRPC
14
+ ---
15
+
16
+ # Backend Skill
17
+
18
+ Comprehensive API, database, and security patterns.
19
+
20
+ ## Quick Reference
21
+
22
+ ### API Design
23
+
24
+ | ID | Rule | Priority |
25
+ |----|------|----------|
26
+ | api-1 | Validate all inputs at boundaries with Zod | CRITICAL |
27
+ | api-2 | Use proper HTTP status codes | CRITICAL |
28
+ | api-3 | Return consistent error format | CRITICAL |
29
+ | api-4 | Never expose internal errors to clients | CRITICAL |
30
+ | api-5 | Use RESTful conventions for endpoints | HIGH |
31
+ | api-6 | Implement rate limiting on public endpoints | HIGH |
32
+ | api-7 | Version APIs when breaking changes needed | HIGH |
33
+ | api-8 | Use pagination for list endpoints | MEDIUM |
34
+ | api-9 | Document endpoints with OpenAPI | MEDIUM |
35
+ | api-10 | Use service layer for business logic | MEDIUM |
36
+
37
+ ### Database (General)
38
+
39
+ | ID | Rule | Priority |
40
+ |----|------|----------|
41
+ | db-1 | Use parameterized queries, never interpolate | CRITICAL |
42
+ | db-2 | Always use transactions for related writes | CRITICAL |
43
+ | db-3 | Prevent N+1 queries with eager loading | HIGH |
44
+ | db-4 | Index columns used in WHERE and JOIN | HIGH |
45
+ | db-5 | Use connection pooling in production | HIGH |
46
+ | db-6 | Add created_at and updated_at to all tables | MEDIUM |
47
+ | db-7 | Use soft deletes for recoverable data | MEDIUM |
48
+ | db-8 | Validate foreign key constraints | MEDIUM |
49
+ | db-9 | Use decimal/string for money, never float | CRITICAL |
50
+ | db-10 | Implement optimistic locking for concurrent updates | MEDIUM |
51
+
52
+ ### PostgreSQL
53
+
54
+ | ID | Rule | Priority |
55
+ |----|------|----------|
56
+ | pg-1 | Use JSONB for flexible schema fields | HIGH |
57
+ | pg-2 | Create partial indexes for filtered queries | HIGH |
58
+ | pg-3 | Use EXPLAIN ANALYZE for query optimization | HIGH |
59
+ | pg-4 | Use UUID v7 for sortable primary keys | MEDIUM |
60
+ | pg-5 | Use CTEs for complex queries | MEDIUM |
61
+ | pg-6 | Use pg_trgm for text search | MEDIUM |
62
+ | pg-7 | Use row-level security for multi-tenancy | HIGH |
63
+ | pg-8 | Set statement_timeout to prevent runaway queries | HIGH |
64
+
65
+ ### MongoDB
66
+
67
+ | ID | Rule | Priority |
68
+ |----|------|----------|
69
+ | mongo-1 | Design schema for query patterns, not normalization | CRITICAL |
70
+ | mongo-2 | Embed frequently accessed related data | HIGH |
71
+ | mongo-3 | Use references for large/unbounded arrays | HIGH |
72
+ | mongo-4 | Create compound indexes matching query patterns | HIGH |
73
+ | mongo-5 | Use aggregation pipeline for complex queries | MEDIUM |
74
+ | mongo-6 | Set maxTimeMS to prevent slow queries | HIGH |
75
+ | mongo-7 | Use change streams for real-time updates | MEDIUM |
76
+ | mongo-8 | Avoid unbounded array growth | CRITICAL |
77
+
78
+ ### Security
79
+
80
+ | ID | Rule | Priority |
81
+ |----|------|----------|
82
+ | sec-1 | Authenticate before authorize | CRITICAL |
83
+ | sec-2 | Check resource ownership on every request | CRITICAL |
84
+ | sec-3 | Never log passwords or tokens | CRITICAL |
85
+ | sec-4 | Use httpOnly cookies for session tokens | CRITICAL |
86
+ | sec-5 | Hash passwords with bcrypt/argon2 | CRITICAL |
87
+ | sec-6 | Implement CSRF protection for mutations | HIGH |
88
+ | sec-7 | Set secure headers (CSP, X-Frame-Options) | HIGH |
89
+ | sec-8 | Sanitize user input before storing | HIGH |
90
+ | sec-9 | Use secrets manager, not env files in production | HIGH |
91
+ | sec-10 | Audit log sensitive operations | MEDIUM |
92
+
93
+ ---
94
+
95
+ ## Critical Rules
96
+
97
+ ### api-1: Input Validation
98
+
99
+ ```typescript
100
+ import { z } from 'zod';
101
+
102
+ const CreateUserSchema = z.object({
103
+ email: z.string().email(),
104
+ password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
105
+ name: z.string().min(2).max(100),
106
+ });
107
+
108
+ export async function POST(request: NextRequest) {
109
+ const body = await request.json();
110
+ const data = CreateUserSchema.parse(body); // Throws on invalid
111
+ // data is now validated and typed
112
+ }
113
+ ```
114
+
115
+ ### api-2: HTTP Status Codes
116
+
117
+ ```
118
+ 200 OK - Success (GET, PUT, PATCH)
119
+ 201 Created - Resource created (POST)
120
+ 204 No Content - Success, no body (DELETE)
121
+ 400 Bad Request - Validation error
122
+ 401 Unauthorized - Authentication required
123
+ 403 Forbidden - Insufficient permissions
124
+ 404 Not Found - Resource not found
125
+ 409 Conflict - Duplicate resource
126
+ 422 Unprocessable - Valid syntax, invalid data
127
+ 429 Too Many - Rate limited
128
+ 500 Internal Error - Server error (hide details)
129
+ ```
130
+
131
+ ### api-3: Error Response Format
132
+
133
+ ```typescript
134
+ // Consistent error format
135
+ interface ErrorResponse {
136
+ error: {
137
+ code: string; // Machine-readable: 'VALIDATION_ERROR'
138
+ message: string; // Human-readable: 'Invalid email format'
139
+ details?: { // Field-level errors
140
+ field: string;
141
+ message: string;
142
+ }[];
143
+ };
144
+ }
145
+
146
+ // Error handler middleware
147
+ function handleError(error: unknown): NextResponse<ErrorResponse> {
148
+ if (error instanceof z.ZodError) {
149
+ return NextResponse.json({
150
+ error: {
151
+ code: 'VALIDATION_ERROR',
152
+ message: 'Invalid input',
153
+ details: error.errors.map(e => ({
154
+ field: e.path.join('.'),
155
+ message: e.message,
156
+ })),
157
+ },
158
+ }, { status: 400 });
159
+ }
160
+
161
+ if (error instanceof AppError) {
162
+ return NextResponse.json({
163
+ error: { code: error.code, message: error.message },
164
+ }, { status: error.statusCode });
165
+ }
166
+
167
+ // Never expose internal errors
168
+ console.error('Unexpected error:', error);
169
+ return NextResponse.json({
170
+ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
171
+ }, { status: 500 });
172
+ }
173
+ ```
174
+
175
+ ### db-1: Parameterized Queries
176
+
177
+ ```typescript
178
+ // GOOD: Prisma handles escaping
179
+ await prisma.user.findMany({
180
+ where: { email: userInput }
181
+ });
182
+
183
+ // GOOD: Raw query with parameters
184
+ await prisma.$queryRaw`
185
+ SELECT * FROM users WHERE email = ${userInput}
186
+ `;
187
+
188
+ // BAD: String interpolation (SQL injection!)
189
+ // await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${userInput}'`);
190
+ ```
191
+
192
+ ### db-2: Transactions
193
+
194
+ ```typescript
195
+ // Related writes must be atomic
196
+ await prisma.$transaction(async (tx) => {
197
+ const user = await tx.user.create({ data: userData });
198
+ await tx.profile.create({ data: { userId: user.id, ...profileData } });
199
+ await tx.auditLog.create({ data: { action: 'USER_CREATED', userId: user.id } });
200
+ return user;
201
+ });
202
+ ```
203
+
204
+ ### db-3: Prevent N+1
205
+
206
+ ```typescript
207
+ // BAD: N+1 queries
208
+ const users = await prisma.user.findMany();
209
+ for (const user of users) {
210
+ const posts = await prisma.post.findMany({ where: { userId: user.id } });
211
+ }
212
+
213
+ // GOOD: Eager loading
214
+ const users = await prisma.user.findMany({
215
+ include: {
216
+ posts: {
217
+ take: 5,
218
+ orderBy: { createdAt: 'desc' },
219
+ },
220
+ },
221
+ });
222
+ ```
223
+
224
+ ### db-9: Money as Decimal
225
+
226
+ ```typescript
227
+ // BAD: Float precision issues
228
+ // 0.1 + 0.2 = 0.30000000000000004
229
+
230
+ // GOOD: Use Decimal or string in cents
231
+ model Transaction {
232
+ amount Decimal @db.Decimal(19, 4)
233
+ currency String @db.VarChar(3)
234
+ }
235
+
236
+ // Or store cents as integer
237
+ model Transaction {
238
+ amountCents Int
239
+ currency String
240
+ }
241
+
242
+ // Display: formatMoney(amountCents / 100)
243
+ ```
244
+
245
+ ---
246
+
247
+ ## PostgreSQL Best Practices
248
+
249
+ ### pg-1: JSONB for Flexible Fields
250
+
251
+ ```sql
252
+ -- Store flexible metadata without schema changes
253
+ CREATE TABLE products (
254
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
255
+ name TEXT NOT NULL,
256
+ metadata JSONB DEFAULT '{}'::jsonb
257
+ );
258
+
259
+ -- Query JSONB fields
260
+ SELECT * FROM products
261
+ WHERE metadata->>'category' = 'electronics'
262
+ AND (metadata->>'price')::numeric > 100;
263
+
264
+ -- Index JSONB for performance
265
+ CREATE INDEX idx_products_metadata ON products USING GIN (metadata);
266
+
267
+ -- Partial index for specific queries
268
+ CREATE INDEX idx_products_category ON products ((metadata->>'category'));
269
+ ```
270
+
271
+ ### pg-2: Partial Indexes
272
+
273
+ ```sql
274
+ -- Index only active users (smaller, faster)
275
+ CREATE INDEX idx_users_active_email ON users (email)
276
+ WHERE deleted_at IS NULL AND status = 'active';
277
+
278
+ -- Index only recent orders
279
+ CREATE INDEX idx_orders_recent ON orders (created_at DESC)
280
+ WHERE created_at > NOW() - INTERVAL '30 days';
281
+
282
+ -- Unique constraint with condition
283
+ CREATE UNIQUE INDEX idx_users_unique_email ON users (email)
284
+ WHERE deleted_at IS NULL;
285
+ ```
286
+
287
+ ### pg-4: UUID v7 for Primary Keys
288
+
289
+ ```sql
290
+ -- UUID v7: timestamp-sortable, better for indexes
291
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
292
+
293
+ -- Drizzle schema
294
+ export const users = pgTable('users', {
295
+ id: uuid('id').primaryKey().defaultRandom(),
296
+ // Or use uuid_generate_v7() with extension
297
+ });
298
+
299
+ -- Benefits over serial:
300
+ -- - No sequence contention
301
+ -- - Can be generated client-side
302
+ -- - Distributed-friendly
303
+ -- - No information leakage
304
+ ```
305
+
306
+ ### pg-7: Row-Level Security for Multi-Tenancy
307
+
308
+ ```sql
309
+ -- Enable RLS
310
+ ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
311
+
312
+ -- Create policy: users see only their org's documents
313
+ CREATE POLICY documents_isolation ON documents
314
+ FOR ALL
315
+ USING (org_id = current_setting('app.current_org_id')::uuid);
316
+
317
+ -- Set org context before queries
318
+ SET app.current_org_id = 'org-uuid-here';
319
+
320
+ -- Now all queries are automatically filtered
321
+ SELECT * FROM documents; -- Only returns current org's docs
322
+ ```
323
+
324
+ ### pg-8: Statement Timeout
325
+
326
+ ```sql
327
+ -- Prevent runaway queries (set in connection)
328
+ SET statement_timeout = '30s';
329
+
330
+ -- Or per-transaction
331
+ BEGIN;
332
+ SET LOCAL statement_timeout = '5s';
333
+ SELECT * FROM expensive_query();
334
+ COMMIT;
335
+
336
+ -- In Prisma connection string
337
+ DATABASE_URL="postgresql://...?statement_timeout=30000"
338
+ ```
339
+
340
+ ---
341
+
342
+ ## MongoDB Best Practices
343
+
344
+ ### mongo-1: Schema Design for Queries
345
+
346
+ ```typescript
347
+ // Design schema based on how you query, not how data relates
348
+
349
+ // BAD: Normalized (requires multiple queries)
350
+ // users collection: { _id, name }
351
+ // orders collection: { _id, userId, items }
352
+ // items collection: { _id, orderId, productId }
353
+
354
+ // GOOD: Denormalized for common query
355
+ // orders collection:
356
+ {
357
+ _id: ObjectId,
358
+ user: { _id, name, email }, // Embedded (frequently accessed)
359
+ items: [
360
+ { productId, name, price, quantity } // Embedded array
361
+ ],
362
+ total: Decimal128,
363
+ status: 'pending'
364
+ }
365
+
366
+ // Query: Get order with all details in ONE query
367
+ const order = await db.orders.findOne({ _id: orderId });
368
+ ```
369
+
370
+ ### mongo-2: Embed vs Reference
371
+
372
+ ```typescript
373
+ // EMBED when:
374
+ // - Data is accessed together
375
+ // - Data belongs to parent
376
+ // - Array is bounded (<100 items)
377
+
378
+ // User with addresses (bounded, always accessed together)
379
+ {
380
+ _id: ObjectId,
381
+ name: 'John',
382
+ addresses: [
383
+ { type: 'home', street: '123 Main' },
384
+ { type: 'work', street: '456 Office' }
385
+ ]
386
+ }
387
+
388
+ // REFERENCE when:
389
+ // - Data is large
390
+ // - Data is accessed independently
391
+ // - Array could grow unbounded
392
+
393
+ // Blog post with comments (unbounded)
394
+ // posts collection
395
+ { _id: ObjectId, title: 'Post', authorId: ObjectId }
396
+
397
+ // comments collection (separate)
398
+ { _id: ObjectId, postId: ObjectId, text: '...', authorId: ObjectId }
399
+ ```
400
+
401
+ ### mongo-4: Compound Indexes
402
+
403
+ ```typescript
404
+ // Index must match query pattern exactly
405
+ // Order matters: equality → sort → range
406
+
407
+ // Query: Find active users, sort by createdAt, filter by age
408
+ db.users.find({ status: 'active', age: { $gte: 18 } }).sort({ createdAt: -1 })
409
+
410
+ // Index: status (equality) → createdAt (sort) → age (range)
411
+ db.users.createIndex({ status: 1, createdAt: -1, age: 1 });
412
+
413
+ // Covered query (index-only, fastest)
414
+ db.users.find(
415
+ { status: 'active' },
416
+ { _id: 0, status: 1, createdAt: 1 } // Only indexed fields
417
+ ).hint({ status: 1, createdAt: -1 });
418
+ ```
419
+
420
+ ### mongo-5: Aggregation Pipeline
421
+
422
+ ```typescript
423
+ // Complex queries with aggregation
424
+ const result = await db.orders.aggregate([
425
+ // Stage 1: Filter
426
+ { $match: { status: 'completed', createdAt: { $gte: lastMonth } } },
427
+
428
+ // Stage 2: Group by user
429
+ { $group: {
430
+ _id: '$userId',
431
+ totalSpent: { $sum: '$total' },
432
+ orderCount: { $sum: 1 }
433
+ }},
434
+
435
+ // Stage 3: Sort by spend
436
+ { $sort: { totalSpent: -1 } },
437
+
438
+ // Stage 4: Top 10
439
+ { $limit: 10 },
440
+
441
+ // Stage 5: Lookup user details
442
+ { $lookup: {
443
+ from: 'users',
444
+ localField: '_id',
445
+ foreignField: '_id',
446
+ as: 'user'
447
+ }},
448
+
449
+ // Stage 6: Reshape
450
+ { $project: {
451
+ userName: { $arrayElemAt: ['$user.name', 0] },
452
+ totalSpent: 1,
453
+ orderCount: 1
454
+ }}
455
+ ]);
456
+ ```
457
+
458
+ ### mongo-8: Avoid Unbounded Arrays
459
+
460
+ ```typescript
461
+ // BAD: Array grows forever
462
+ {
463
+ _id: 'popular-post',
464
+ likes: ['user1', 'user2', ... 'user1000000'] // Document too large!
465
+ }
466
+
467
+ // GOOD: Bucket pattern (fixed array size)
468
+ {
469
+ _id: ObjectId,
470
+ postId: 'popular-post',
471
+ bucket: 1,
472
+ likes: ['user1', ..., 'user100'], // Max 100 per bucket
473
+ count: 100
474
+ }
475
+
476
+ // GOOD: Separate collection
477
+ // likes collection
478
+ { postId: 'popular-post', userId: 'user1', createdAt: Date }
479
+
480
+ // Count with aggregation or maintain counter
481
+ { _id: 'popular-post', likeCount: 1000000 }
482
+ ```
483
+
484
+ ### sec-2: Check Resource Ownership
485
+
486
+ ```typescript
487
+ export async function DELETE(
488
+ request: NextRequest,
489
+ { params }: { params: { id: string } }
490
+ ) {
491
+ const session = await getSession();
492
+ if (!session) {
493
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
494
+ }
495
+
496
+ const resource = await prisma.resource.findUnique({
497
+ where: { id: params.id },
498
+ });
499
+
500
+ if (!resource) {
501
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
502
+ }
503
+
504
+ // CRITICAL: Check ownership
505
+ if (resource.userId !== session.user.id && session.user.role !== 'ADMIN') {
506
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
507
+ }
508
+
509
+ await prisma.resource.delete({ where: { id: params.id } });
510
+ return NextResponse.json({ success: true });
511
+ }
512
+ ```
513
+
514
+ ### sec-4: Secure Session Cookies
515
+
516
+ ```typescript
517
+ const sessionOptions = {
518
+ password: process.env.SESSION_SECRET!, // 32+ chars
519
+ cookieName: 'session',
520
+ cookieOptions: {
521
+ httpOnly: true, // No JS access
522
+ secure: true, // HTTPS only
523
+ sameSite: 'lax', // CSRF protection
524
+ maxAge: 60 * 60 * 24, // 24 hours
525
+ path: '/',
526
+ },
527
+ };
528
+ ```
529
+
530
+ ### sec-5: Password Hashing
531
+
532
+ ```typescript
533
+ import { hash, verify } from '@node-rs/argon2';
534
+
535
+ // Hash password before storing
536
+ const hashedPassword = await hash(plainPassword, {
537
+ memoryCost: 65536,
538
+ timeCost: 3,
539
+ parallelism: 4,
540
+ });
541
+
542
+ // Verify on login
543
+ const isValid = await verify(hashedPassword, inputPassword);
544
+ ```
545
+
546
+ ---
547
+
548
+ ## API Patterns
549
+
550
+ ### RESTful Endpoints
551
+
552
+ ```
553
+ GET /users # List users
554
+ GET /users/:id # Get user
555
+ POST /users # Create user
556
+ PATCH /users/:id # Update user
557
+ DELETE /users/:id # Delete user
558
+
559
+ GET /users/:id/posts # Nested resources
560
+ POST /users/:id/posts
561
+
562
+ # Query params for filtering
563
+ GET /users?role=admin&status=active&page=2&limit=20&sort=-createdAt
564
+ ```
565
+
566
+ ### Pagination
567
+
568
+ ```typescript
569
+ const PaginationSchema = z.object({
570
+ page: z.coerce.number().min(1).default(1),
571
+ limit: z.coerce.number().min(1).max(100).default(20),
572
+ });
573
+
574
+ export async function GET(request: NextRequest) {
575
+ const params = Object.fromEntries(request.nextUrl.searchParams);
576
+ const { page, limit } = PaginationSchema.parse(params);
577
+
578
+ const [data, total] = await Promise.all([
579
+ prisma.user.findMany({
580
+ skip: (page - 1) * limit,
581
+ take: limit,
582
+ }),
583
+ prisma.user.count(),
584
+ ]);
585
+
586
+ return NextResponse.json({
587
+ data,
588
+ meta: {
589
+ page,
590
+ limit,
591
+ total,
592
+ totalPages: Math.ceil(total / limit),
593
+ },
594
+ });
595
+ }
596
+ ```
597
+
598
+ ### Service Layer
599
+
600
+ ```typescript
601
+ // services/user.service.ts
602
+ export class UserService {
603
+ async create(input: CreateUserInput): Promise<User> {
604
+ const existing = await prisma.user.findUnique({
605
+ where: { email: input.email },
606
+ });
607
+
608
+ if (existing) {
609
+ throw new ConflictError('Email already exists');
610
+ }
611
+
612
+ return prisma.user.create({
613
+ data: {
614
+ ...input,
615
+ password: await hash(input.password),
616
+ },
617
+ });
618
+ }
619
+
620
+ async findById(id: string): Promise<User> {
621
+ const user = await prisma.user.findUnique({ where: { id } });
622
+ if (!user) throw new NotFoundError('User');
623
+ return user;
624
+ }
625
+ }
626
+ ```
627
+
628
+ ### Rate Limiting
629
+
630
+ ```typescript
631
+ import { Ratelimit } from '@upstash/ratelimit';
632
+ import { Redis } from '@upstash/redis';
633
+
634
+ const ratelimit = new Ratelimit({
635
+ redis: Redis.fromEnv(),
636
+ limiter: Ratelimit.slidingWindow(10, '10s'),
637
+ });
638
+
639
+ export async function POST(request: NextRequest) {
640
+ const ip = request.ip ?? '127.0.0.1';
641
+ const { success, remaining } = await ratelimit.limit(ip);
642
+
643
+ if (!success) {
644
+ return NextResponse.json(
645
+ { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
646
+ { status: 429, headers: { 'X-RateLimit-Remaining': String(remaining) } }
647
+ );
648
+ }
649
+
650
+ // Process request...
651
+ }
652
+ ```
653
+
654
+ ---
655
+
656
+ ## Error Classes
657
+
658
+ ```typescript
659
+ export class AppError extends Error {
660
+ constructor(
661
+ public code: string,
662
+ message: string,
663
+ public statusCode: number = 500
664
+ ) {
665
+ super(message);
666
+ this.name = 'AppError';
667
+ }
668
+ }
669
+
670
+ export class ValidationError extends AppError {
671
+ constructor(message: string) {
672
+ super('VALIDATION_ERROR', message, 400);
673
+ }
674
+ }
675
+
676
+ export class NotFoundError extends AppError {
677
+ constructor(resource: string) {
678
+ super('NOT_FOUND', `${resource} not found`, 404);
679
+ }
680
+ }
681
+
682
+ export class UnauthorizedError extends AppError {
683
+ constructor() {
684
+ super('UNAUTHORIZED', 'Authentication required', 401);
685
+ }
686
+ }
687
+
688
+ export class ForbiddenError extends AppError {
689
+ constructor() {
690
+ super('FORBIDDEN', 'Insufficient permissions', 403);
691
+ }
692
+ }
693
+
694
+ export class ConflictError extends AppError {
695
+ constructor(message: string) {
696
+ super('CONFLICT', message, 409);
697
+ }
698
+ }
699
+ ```
700
+
701
+ ---
702
+
703
+ ## Logging
704
+
705
+ ```typescript
706
+ // Never log sensitive data
707
+ const logger = {
708
+ info: (message: string, meta?: object) => {
709
+ console.log(JSON.stringify({
710
+ level: 'info',
711
+ message,
712
+ ...meta,
713
+ timestamp: new Date().toISOString(),
714
+ }));
715
+ },
716
+
717
+ error: (message: string, error?: Error) => {
718
+ console.error(JSON.stringify({
719
+ level: 'error',
720
+ message,
721
+ error: error ? {
722
+ name: error.name,
723
+ message: error.message,
724
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
725
+ } : undefined,
726
+ timestamp: new Date().toISOString(),
727
+ }));
728
+ },
729
+ };
730
+
731
+ // Usage
732
+ logger.info('User created', { userId: user.id }); // Good
733
+ // logger.info('User created', { email, password }); // BAD!
734
+ ```