compress-claude 1.0.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.
@@ -0,0 +1,3025 @@
1
+ "use strict";
2
+ // ---- generator.ts ----
3
+ // Generated context files (CLAUDE.md, ai-docs/) for Claude Code sessions.
4
+ // Converted from claude-context.js to TypeScript.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.EXTRA_FILES = exports.SKILL_FILES = exports.ALL_DOMAIN_DOCS = void 0;
40
+ exports.generateContextFiles = generateContextFiles;
41
+ exports.claudeMd = claudeMd;
42
+ exports.generateDomainDoc = generateDomainDoc;
43
+ exports.getDomainFiles = getDomainFiles;
44
+ exports.generatePromptFile = generatePromptFile;
45
+ exports.getPromptFiles = getPromptFiles;
46
+ exports.generateSkillFile = generateSkillFile;
47
+ exports.generateExtraFile = generateExtraFile;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const logger_1 = require("./utils/logger");
51
+ // ---------------------------------------------------------------------------
52
+ // Prompts Index
53
+ // ---------------------------------------------------------------------------
54
+ function promptsIndex(stack) {
55
+ return `# PROMPTS.md — Pattern Library Index
56
+
57
+ > Before writing any non-trivial code, check this index.
58
+ > Load the relevant category file, find the pattern, follow Build → Verify → Debug.
59
+ > This prevents hallucinated APIs, broken patterns, and wasted tokens.
60
+
61
+ ## How to Use
62
+ 1. Find your task in the index below
63
+ 2. Load the category file: \`Read /ai-docs/prompts/[category].md\`
64
+ 3. Follow the Build prompt exactly
65
+ 4. Run the Verify checklist before moving on
66
+ 5. If broken, use the Debug guide — don't guess
67
+
68
+ ## Categories
69
+
70
+ | Category | File | Covers |
71
+ |----------|------|--------|
72
+ | Auth | \`/ai-docs/prompts/auth.md\` | Sign up, sign in, magic link, OAuth, session, middleware |
73
+ | Database | \`/ai-docs/prompts/database.md\` | Schema, queries, migrations, relations, transactions |
74
+ | Payments | \`/ai-docs/prompts/payments.md\` | Checkout, webhooks, portal, subscription gates, refunds |
75
+ | API | \`/ai-docs/prompts/api.md\` | Route handlers, validation, errors, rate limiting, middleware |
76
+ | Email | \`/ai-docs/prompts/email.md\` | Templates, triggers, testing, DNS |
77
+ | UI | \`/ai-docs/prompts/ui.md\` | Pages, components, forms, tables, modals, loading states |
78
+ | Deployment | \`/ai-docs/prompts/deployment.md\` | Railway deploy, env vars, crons, health checks, rollback |
79
+ | Analytics | \`/ai-docs/prompts/analytics.md\` | GA4 events, Search Console, conversions, sitemap |
80
+ | Security | \`/ai-docs/prompts/security.md\` | Auth checks, input validation, rate limiting, OWASP |
81
+ | Testing | \`/ai-docs/prompts/testing.md\` | Unit tests, integration, mocks, coverage |
82
+
83
+ ## Quick Reference — Most Common Tasks
84
+
85
+ \`\`\`
86
+ Adding a new page → ui.md → "New Route/Page"
87
+ Adding a new API endpoint → api.md → "New Route Handler"
88
+ Changing DB schema → database.md → "Schema Change"
89
+ Adding auth to a route → auth.md → "Protect a Route"
90
+ New Stripe webhook event → payments.md → "Handle Webhook Event"
91
+ Sending a transactional email → email.md → "Send Transactional Email"
92
+ New cron job → deployment.md → "Add Cron Job"
93
+ Tracking a conversion → analytics.md → "Track Custom Event"
94
+ \`\`\`
95
+
96
+ ## Token-Saving Rules
97
+ - Load ONE category file at a time — don't load all of them
98
+ - Only load domain docs relevant to the current task
99
+ - After finishing a task, you can unload the category file
100
+ - Check MISTAKES.md before starting any task you've attempted before
101
+ `;
102
+ }
103
+ ;
104
+ // ---------------------------------------------------------------------------
105
+ // Prompts Auth
106
+ // ---------------------------------------------------------------------------
107
+ function authPrompts(stack) {
108
+ const auth = stack.auth || 'custom auth';
109
+ const isClerk = auth.includes('Clerk');
110
+ const isNextAuth = auth.includes('NextAuth');
111
+ const isSupabase = auth.includes('Supabase');
112
+ return `# Auth Prompts
113
+
114
+ ## Protect a Route (Middleware)
115
+
116
+ ### Build
117
+ ${isClerk ? `\`\`\`typescript
118
+ // middleware.ts (root level)
119
+ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
120
+
121
+ const isPublicRoute = createRouteMatcher([
122
+ '/', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)', '/api/health'
123
+ ]);
124
+
125
+ export default clerkMiddleware((auth, req) => {
126
+ if (!isPublicRoute(req)) auth().protect();
127
+ });
128
+
129
+ export const config = { matcher: ['/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)'] };
130
+ \`\`\`` : ''}
131
+ ${isNextAuth ? `\`\`\`typescript
132
+ // middleware.ts
133
+ export { default } from 'next-auth/middleware';
134
+ export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'] };
135
+ \`\`\`` : ''}
136
+
137
+ ### Verify
138
+ - [ ] Public routes accessible without auth (/, /sign-in, /api/health, /api/webhooks/*)
139
+ - [ ] Protected routes redirect to sign-in when unauthenticated
140
+ - [ ] API routes return 401 JSON (not redirect) when unauthenticated
141
+ - [ ] Webhook endpoints explicitly excluded from auth middleware
142
+
143
+ ### Debug
144
+ - Route redirecting when it shouldn't → check \`isPublicRoute\` matcher patterns
145
+ - Webhook 401s → ensure \`/api/webhooks(.*)\` is in public routes
146
+ - Middleware not running → check \`config.matcher\` excludes static files
147
+
148
+ ---
149
+
150
+ ## Get Current User (Server Component)
151
+
152
+ ### Build
153
+ ${isClerk ? `\`\`\`typescript
154
+ import { auth, currentUser } from '@clerk/nextjs/server';
155
+
156
+ // Just the ID (fastest — use this for auth checks)
157
+ const { userId } = auth();
158
+ if (!userId) redirect('/sign-in');
159
+
160
+ // Full user object (slower — only when you need name/email)
161
+ const user = await currentUser();
162
+ \`\`\`` : ''}
163
+ ${isNextAuth ? `\`\`\`typescript
164
+ import { getServerSession } from 'next-auth';
165
+ import { authOptions } from '@/lib/auth';
166
+
167
+ const session = await getServerSession(authOptions);
168
+ if (!session) redirect('/sign-in');
169
+ const { user } = session; // user.id, user.email, user.role
170
+ \`\`\`` : ''}
171
+ ${isSupabase ? `\`\`\`typescript
172
+ import { createServerClient } from '@supabase/auth-helpers-nextjs';
173
+ import { cookies } from 'next/headers';
174
+
175
+ const supabase = createServerClient(
176
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
177
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
178
+ { cookies: { get: (name) => cookies().get(name)?.value } }
179
+ );
180
+ const { data: { session } } = await supabase.auth.getSession();
181
+ if (!session) redirect('/sign-in');
182
+ \`\`\`` : ''}
183
+
184
+ ### Verify
185
+ - [ ] Unauthenticated users redirected correctly
186
+ - [ ] userId/session is typed (not \`any\`)
187
+ - [ ] Not calling \`currentUser()\` when you only need \`userId\` (costs extra request)
188
+
189
+ ### Debug
190
+ - \`userId\` is null in middleware but user is signed in → session cookie missing, check domain config
191
+ - \`currentUser()\` returns null → user exists in Clerk but not synced, check webhook
192
+
193
+ ---
194
+
195
+ ## Get Current User (API Route / Server Action)
196
+
197
+ ### Build
198
+ ${isClerk ? `\`\`\`typescript
199
+ // In API route or Server Action
200
+ import { auth } from '@clerk/nextjs/server';
201
+
202
+ export async function POST(req: Request) {
203
+ const { userId } = auth();
204
+ if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
205
+ // ... rest of handler
206
+ }
207
+ \`\`\`` : ''}
208
+
209
+ ### Verify
210
+ - [ ] Returns 401 JSON (not redirect) for API routes
211
+ - [ ] userId checked BEFORE any DB query
212
+ - [ ] Never trust userId from request body — always from session
213
+
214
+ ---
215
+
216
+ ## Sync User to Database (Webhook)
217
+
218
+ ### Build
219
+ ${isClerk ? `\`\`\`typescript
220
+ // app/api/webhooks/clerk/route.ts
221
+ import { Webhook } from 'svix';
222
+ import { headers } from 'next/headers';
223
+ import { db } from '@/lib/db';
224
+
225
+ export async function POST(req: Request) {
226
+ const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
227
+ if (!WEBHOOK_SECRET) throw new Error('Missing CLERK_WEBHOOK_SECRET');
228
+
229
+ const payload = await req.json();
230
+ const headerPayload = headers();
231
+ const svixHeaders = {
232
+ 'svix-id': headerPayload.get('svix-id')!,
233
+ 'svix-timestamp': headerPayload.get('svix-timestamp')!,
234
+ 'svix-signature': headerPayload.get('svix-signature')!,
235
+ };
236
+
237
+ const wh = new Webhook(WEBHOOK_SECRET);
238
+ let evt: any;
239
+ try {
240
+ evt = wh.verify(JSON.stringify(payload), svixHeaders);
241
+ } catch {
242
+ return Response.json({ error: 'Invalid signature' }, { status: 400 });
243
+ }
244
+
245
+ if (evt.type === 'user.created') {
246
+ await db.user.create({
247
+ data: {
248
+ clerkId: evt.data.id,
249
+ email: evt.data.email_addresses[0].email_address,
250
+ name: \`\${evt.data.first_name} \${evt.data.last_name}\`.trim(),
251
+ },
252
+ });
253
+ }
254
+
255
+ if (evt.type === 'user.deleted') {
256
+ await db.user.update({
257
+ where: { clerkId: evt.data.id },
258
+ data: { deletedAt: new Date() },
259
+ });
260
+ }
261
+
262
+ return Response.json({ received: true });
263
+ }
264
+ \`\`\`` : ''}
265
+
266
+ ### Verify
267
+ - [ ] Webhook signature verified before processing
268
+ - [ ] Idempotent — re-running with same event doesn't create duplicates
269
+ - [ ] \`user.created\`, \`user.updated\`, \`user.deleted\` all handled
270
+ - [ ] Webhook secret in env vars, not hardcoded
271
+ - [ ] Route in public routes (not behind auth middleware)
272
+
273
+ ### Debug
274
+ - 400 on webhook → signature mismatch, check \`CLERK_WEBHOOK_SECRET\` matches dashboard
275
+ - User not in DB → webhook not configured in Clerk dashboard, check endpoint URL
276
+ - Duplicate users → missing idempotency check, add \`upsert\` instead of \`create\`
277
+
278
+ ---
279
+
280
+ ## Role-Based Access Control
281
+
282
+ ### Build
283
+ \`\`\`typescript
284
+ // src/lib/permissions.ts
285
+ export type Role = 'admin' | 'user' | 'viewer';
286
+
287
+ export const can = {
288
+ viewDashboard: (role: Role) => ['admin', 'user'].includes(role),
289
+ manageUsers: (role: Role) => role === 'admin',
290
+ exportData: (role: Role) => ['admin', 'user'].includes(role),
291
+ };
292
+
293
+ // Usage in Server Component or API route
294
+ const user = await getUserFromDb(userId);
295
+ if (!can.manageUsers(user.role)) {
296
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
297
+ }
298
+ \`\`\`
299
+
300
+ ### Verify
301
+ - [ ] Role checked server-side, never trusted from client
302
+ - [ ] 403 returned (not 401) for wrong role — user is authenticated, just not authorized
303
+ - [ ] Role stored in DB, not in JWT claims (claims can be stale)
304
+
305
+ ### Debug
306
+ - Role check passing when it shouldn't → pulling role from JWT instead of DB, fix to query DB
307
+ `;
308
+ }
309
+ ;
310
+ // ---------------------------------------------------------------------------
311
+ // Prompts Database
312
+ // ---------------------------------------------------------------------------
313
+ function databasePrompts(stack) {
314
+ const db = stack.database || 'Prisma (PostgreSQL)';
315
+ const isPrisma = db.includes('Prisma');
316
+ const isDrizzle = db.includes('Drizzle');
317
+ return `# Database Prompts
318
+
319
+ ## Schema Change (Add Table or Column)
320
+
321
+ ### Build
322
+ ${isPrisma ? `1. Edit \`prisma/schema.prisma\`
323
+ 2. Run \`pnpm db:push\` (dev) or \`pnpm db:migrate\` (staging/prod)
324
+ 3. Regenerate client: \`pnpm prisma generate\`
325
+
326
+ \`\`\`prisma
327
+ // New table pattern
328
+ model Post {
329
+ id String @id @default(cuid())
330
+ title String
331
+ content String?
332
+ published Boolean @default(false)
333
+ authorId String
334
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
335
+ createdAt DateTime @default(now())
336
+ updatedAt DateTime @updatedAt
337
+ deletedAt DateTime? // soft delete
338
+
339
+ @@index([authorId])
340
+ @@index([createdAt])
341
+ }
342
+ \`\`\`` : ''}
343
+ ${isDrizzle ? `1. Edit \`src/db/schema.ts\`
344
+ 2. Generate migration: \`pnpm drizzle-kit generate\`
345
+ 3. Run migration: \`pnpm drizzle-kit migrate\`
346
+
347
+ \`\`\`typescript
348
+ // src/db/schema.ts
349
+ import { pgTable, text, boolean, timestamp, index } from 'drizzle-orm/pg-core';
350
+ import { createId } from '@paralleldrive/cuid2';
351
+
352
+ export const posts = pgTable('posts', {
353
+ id: text('id').primaryKey().$defaultFn(() => createId()),
354
+ title: text('title').notNull(),
355
+ content: text('content'),
356
+ published: boolean('published').default(false).notNull(),
357
+ authorId: text('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
358
+ createdAt: timestamp('created_at').defaultNow().notNull(),
359
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
360
+ deletedAt: timestamp('deleted_at'),
361
+ }, (table) => ({
362
+ authorIdx: index('posts_author_idx').on(table.authorId),
363
+ createdAtIdx: index('posts_created_at_idx').on(table.createdAt),
364
+ }));
365
+ \`\`\`` : ''}
366
+
367
+ ### Verify
368
+ - [ ] Every table has: id, createdAt, updatedAt
369
+ - [ ] Foreign keys have \`onDelete\` set (Cascade or SetNull — never default)
370
+ - [ ] Indexes added for: all foreign keys + frequently filtered/sorted columns
371
+ - [ ] Migration file generated and committed (don't skip migration for prod)
372
+ - [ ] \`pnpm prisma generate\` run after schema change
373
+
374
+ ### Debug
375
+ - Migration drift → \`pnpm prisma migrate reset\` (dev only!) then re-migrate
376
+ - Type errors after schema change → forgot to run \`prisma generate\`
377
+ - Slow queries → check missing indexes with \`EXPLAIN ANALYZE\` in Railway Postgres
378
+
379
+ ---
380
+
381
+ ## Query — Fetch with Relations
382
+
383
+ ### Build
384
+ ${isPrisma ? `\`\`\`typescript
385
+ // ✅ Correct — select only what you need
386
+ const posts = await prisma.post.findMany({
387
+ where: { authorId: userId, deletedAt: null },
388
+ select: {
389
+ id: true,
390
+ title: true,
391
+ published: true,
392
+ createdAt: true,
393
+ author: { select: { id: true, name: true } },
394
+ },
395
+ orderBy: { createdAt: 'desc' },
396
+ take: 20, // always paginate
397
+ skip: page * 20,
398
+ });
399
+
400
+ // ❌ Wrong — never do this
401
+ const posts = await prisma.post.findMany(); // unbounded, returns everything
402
+ \`\`\`` : ''}
403
+ ${isDrizzle ? `\`\`\`typescript
404
+ // ✅ Correct
405
+ const posts = await db
406
+ .select({
407
+ id: posts.id,
408
+ title: posts.title,
409
+ published: posts.published,
410
+ createdAt: posts.createdAt,
411
+ authorName: users.name,
412
+ })
413
+ .from(posts)
414
+ .leftJoin(users, eq(posts.authorId, users.id))
415
+ .where(and(eq(posts.authorId, userId), isNull(posts.deletedAt)))
416
+ .orderBy(desc(posts.createdAt))
417
+ .limit(20)
418
+ .offset(page * 20);
419
+ \`\`\`` : ''}
420
+
421
+ ### Verify
422
+ - [ ] Query is paginated (never unbounded)
423
+ - [ ] Only selecting needed fields (not \`select *\` / full object)
424
+ - [ ] \`deletedAt: null\` filter if using soft deletes
425
+ - [ ] Sorted by a consistent field for stable pagination
426
+
427
+ ### Debug
428
+ - N+1 queries → use \`include\`/\`join\` instead of looping and querying per item
429
+ - Slow query → add missing index, or check if filtering on non-indexed column
430
+
431
+ ---
432
+
433
+ ## Multi-Step Write (Transaction)
434
+
435
+ ### Build
436
+ ${isPrisma ? `\`\`\`typescript
437
+ // Always use transactions for multi-step writes
438
+ const result = await prisma.$transaction(async (tx) => {
439
+ const user = await tx.user.update({
440
+ where: { id: userId },
441
+ data: { credits: { decrement: 1 } },
442
+ });
443
+
444
+ if (user.credits < 0) {
445
+ throw new Error('INSUFFICIENT_CREDITS'); // rolls back automatically
446
+ }
447
+
448
+ const job = await tx.job.create({
449
+ data: { userId, status: 'pending' },
450
+ });
451
+
452
+ return job;
453
+ });
454
+ \`\`\`` : ''}
455
+ ${isDrizzle ? `\`\`\`typescript
456
+ const result = await db.transaction(async (tx) => {
457
+ const [updated] = await tx
458
+ .update(users)
459
+ .set({ credits: sql\`credits - 1\` })
460
+ .where(eq(users.id, userId))
461
+ .returning();
462
+
463
+ if (updated.credits < 0) throw new Error('INSUFFICIENT_CREDITS');
464
+
465
+ const [job] = await tx.insert(jobs).values({ userId, status: 'pending' }).returning();
466
+ return job;
467
+ });
468
+ \`\`\`` : ''}
469
+
470
+ ### Verify
471
+ - [ ] All related writes inside a single transaction
472
+ - [ ] Throws on invalid state (so transaction rolls back)
473
+ - [ ] Return value is the final needed object, not intermediate results
474
+
475
+ ### Debug
476
+ - Transaction timeout → operations inside are too slow, add indexes or split into background job
477
+ - Deadlock → two transactions updating same rows in different order, standardize update order
478
+
479
+ ---
480
+
481
+ ## Soft Delete Pattern
482
+
483
+ ### Build
484
+ \`\`\`typescript
485
+ // Delete (soft)
486
+ await prisma.post.update({
487
+ where: { id: postId, authorId: userId }, // scope to owner!
488
+ data: { deletedAt: new Date() },
489
+ });
490
+
491
+ // Always filter out deleted records in queries
492
+ where: { deletedAt: null }
493
+
494
+ // Hard delete (only for GDPR compliance / user data requests)
495
+ await prisma.post.delete({ where: { id: postId } });
496
+ \`\`\`
497
+
498
+ ### Verify
499
+ - [ ] Soft delete scoped to owner (include \`authorId: userId\` in where)
500
+ - [ ] All list queries include \`deletedAt: null\` filter
501
+ - [ ] Hard delete available for GDPR requests only
502
+
503
+ ---
504
+
505
+ ## Seeding
506
+
507
+ ### Build
508
+ \`\`\`typescript
509
+ // prisma/seed.ts or scripts/seed.ts
510
+ async function main() {
511
+ // Upsert so seed is idempotent (safe to re-run)
512
+ const admin = await prisma.user.upsert({
513
+ where: { email: 'admin@example.com' },
514
+ update: {},
515
+ create: {
516
+ email: 'admin@example.com',
517
+ name: 'Admin',
518
+ role: 'admin',
519
+ },
520
+ });
521
+ console.log('Seeded:', admin.email);
522
+ }
523
+
524
+ main().catch(console.error).finally(() => prisma.$disconnect());
525
+ \`\`\`
526
+
527
+ ### Verify
528
+ - [ ] Seed is idempotent — upsert not create
529
+ - [ ] No hardcoded passwords or real data
530
+ - [ ] Can be re-run safely in CI
531
+ `;
532
+ }
533
+ ;
534
+ // ---------------------------------------------------------------------------
535
+ // Prompts Payments
536
+ // ---------------------------------------------------------------------------
537
+ function paymentsPrompts(stack) {
538
+ return `# Payments Prompts
539
+
540
+ ## Create Checkout Session
541
+
542
+ ### Build
543
+ \`\`\`typescript
544
+ // app/api/checkout/route.ts
545
+ import { stripe } from '@/lib/stripe';
546
+ import { auth } from '@clerk/nextjs/server';
547
+ import { db } from '@/lib/db';
548
+
549
+ export async function POST(req: Request) {
550
+ const { userId } = auth();
551
+ if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
552
+
553
+ const { priceId } = await req.json();
554
+ const user = await db.user.findUnique({ where: { clerkId: userId } });
555
+ if (!user) return Response.json({ error: 'User not found' }, { status: 404 });
556
+
557
+ // Ensure Stripe customer exists
558
+ let customerId = user.stripeCustomerId;
559
+ if (!customerId) {
560
+ const customer = await stripe.customers.create({
561
+ email: user.email,
562
+ metadata: { userId: user.id },
563
+ });
564
+ customerId = customer.id;
565
+ await db.user.update({ where: { id: user.id }, data: { stripeCustomerId: customerId } });
566
+ }
567
+
568
+ const session = await stripe.checkout.sessions.create({
569
+ customer: customerId,
570
+ mode: 'subscription',
571
+ payment_method_types: ['card'],
572
+ line_items: [{ price: priceId, quantity: 1 }],
573
+ success_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true\`,
574
+ cancel_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/pricing\`,
575
+ metadata: { userId: user.id },
576
+ });
577
+
578
+ return Response.json({ url: session.url });
579
+ }
580
+ \`\`\`
581
+
582
+ ### Verify
583
+ - [ ] Auth checked before creating session
584
+ - [ ] Customer ID saved to DB (don't create duplicate customers)
585
+ - [ ] \`success_url\` and \`cancel_url\` use env var, not hardcoded domain
586
+ - [ ] Metadata includes \`userId\` for webhook reconciliation
587
+ - [ ] Works in both test mode (test keys) and live mode
588
+
589
+ ### Debug
590
+ - Customer already exists error → check \`stripeCustomerId\` in DB before creating
591
+ - Redirect after checkout goes nowhere → check \`success_url\` env var is set in Railway
592
+ - Webhook not updating subscription → confirm webhook is pointed at prod URL, not localhost
593
+
594
+ ---
595
+
596
+ ## Handle Webhook Events
597
+
598
+ ### Build
599
+ \`\`\`typescript
600
+ // app/api/webhooks/stripe/route.ts
601
+ import Stripe from 'stripe';
602
+ import { stripe } from '@/lib/stripe';
603
+ import { db } from '@/lib/db';
604
+
605
+ export async function POST(req: Request) {
606
+ const body = await req.text(); // MUST be text, not json()
607
+ const sig = req.headers.get('stripe-signature')!;
608
+
609
+ let event: Stripe.Event;
610
+ try {
611
+ event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
612
+ } catch {
613
+ return Response.json({ error: 'Invalid signature' }, { status: 400 });
614
+ }
615
+
616
+ // Idempotency: check if already processed
617
+ const processed = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
618
+ if (processed) return Response.json({ received: true });
619
+
620
+ try {
621
+ switch (event.type) {
622
+ case 'checkout.session.completed': {
623
+ const session = event.data.object as Stripe.CheckoutSession;
624
+ await handleCheckoutComplete(session);
625
+ break;
626
+ }
627
+ case 'customer.subscription.updated':
628
+ case 'customer.subscription.deleted': {
629
+ const sub = event.data.object as Stripe.Subscription;
630
+ await handleSubscriptionChange(sub);
631
+ break;
632
+ }
633
+ case 'invoice.payment_failed': {
634
+ const invoice = event.data.object as Stripe.Invoice;
635
+ await handlePaymentFailed(invoice);
636
+ break;
637
+ }
638
+ }
639
+
640
+ // Mark as processed
641
+ await db.webhookEvent.create({ data: { stripeEventId: event.id, type: event.type } });
642
+ } catch (error) {
643
+ console.error('[STRIPE_WEBHOOK]', event.type, error);
644
+ return Response.json({ error: 'Webhook handler failed' }, { status: 500 });
645
+ }
646
+
647
+ return Response.json({ received: true });
648
+ }
649
+
650
+ async function handleCheckoutComplete(session: Stripe.CheckoutSession) {
651
+ const userId = session.metadata?.userId;
652
+ if (!userId) throw new Error('No userId in session metadata');
653
+
654
+ const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
655
+ await db.user.update({
656
+ where: { id: userId },
657
+ data: {
658
+ subscriptionId: subscription.id,
659
+ subscriptionStatus: subscription.status,
660
+ subscriptionPriceId: subscription.items.data[0].price.id,
661
+ subscriptionCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
662
+ },
663
+ });
664
+ }
665
+
666
+ async function handleSubscriptionChange(sub: Stripe.Subscription) {
667
+ await db.user.update({
668
+ where: { stripeCustomerId: sub.customer as string },
669
+ data: {
670
+ subscriptionStatus: sub.status,
671
+ subscriptionPriceId: sub.items.data[0]?.price.id ?? null,
672
+ subscriptionCurrentPeriodEnd: new Date(sub.current_period_end * 1000),
673
+ },
674
+ });
675
+ }
676
+
677
+ async function handlePaymentFailed(invoice: Stripe.Invoice) {
678
+ // Send dunning email via Resend
679
+ const user = await db.user.findUnique({ where: { stripeCustomerId: invoice.customer as string } });
680
+ if (user) {
681
+ // await sendPaymentFailedEmail(user.email);
682
+ console.log('[PAYMENT_FAILED] User:', user.email);
683
+ }
684
+ }
685
+ \`\`\`
686
+
687
+ ### Verify
688
+ - [ ] \`req.text()\` used (not \`req.json()\`) — signature breaks with json parsing
689
+ - [ ] Signature verified BEFORE processing
690
+ - [ ] Idempotency check — same event can arrive multiple times
691
+ - [ ] All relevant event types handled: checkout.completed, subscription.updated, subscription.deleted, invoice.payment_failed
692
+ - [ ] \`STRIPE_WEBHOOK_SECRET\` is the webhook endpoint secret (not API key)
693
+ - [ ] Route excluded from auth middleware
694
+
695
+ ### Debug
696
+ - 400 on webhook → almost always \`req.json()\` instead of \`req.text()\`
697
+ - Duplicate subscription updates → missing idempotency check
698
+ - Wrong secret → using API key instead of webhook endpoint secret (get it from Stripe dashboard → Webhooks → your endpoint)
699
+
700
+ ---
701
+
702
+ ## Subscription Gate (Feature Access)
703
+
704
+ ### Build
705
+ \`\`\`typescript
706
+ // src/lib/subscription.ts
707
+ export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid';
708
+
709
+ export function isSubscriptionActive(status: SubscriptionStatus | null): boolean {
710
+ return status === 'active' || status === 'trialing';
711
+ }
712
+
713
+ // Usage in Server Component
714
+ const user = await db.user.findUnique({ where: { clerkId: userId } });
715
+ if (!isSubscriptionActive(user?.subscriptionStatus)) {
716
+ redirect('/pricing?reason=subscription_required');
717
+ }
718
+
719
+ // Usage in API route
720
+ if (!isSubscriptionActive(user?.subscriptionStatus)) {
721
+ return Response.json({ error: 'Subscription required', code: 'SUBSCRIPTION_REQUIRED' }, { status: 403 });
722
+ }
723
+ \`\`\`
724
+
725
+ ### Verify
726
+ - [ ] Check subscription in DB — never trust client-side claims
727
+ - [ ] \`trialing\` treated same as \`active\` for access
728
+ - [ ] \`past_due\` shows warning but doesn't fully lock out (give grace period)
729
+ - [ ] Redirect includes reason param so pricing page can show contextual message
730
+
731
+ ---
732
+
733
+ ## Customer Billing Portal
734
+
735
+ ### Build
736
+ \`\`\`typescript
737
+ // app/api/billing/portal/route.ts
738
+ export async function POST(req: Request) {
739
+ const { userId } = auth();
740
+ if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
741
+
742
+ const user = await db.user.findUnique({ where: { clerkId: userId } });
743
+ if (!user?.stripeCustomerId) {
744
+ return Response.json({ error: 'No billing account found' }, { status: 404 });
745
+ }
746
+
747
+ const session = await stripe.billingPortal.sessions.create({
748
+ customer: user.stripeCustomerId,
749
+ return_url: \`\${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings\`,
750
+ });
751
+
752
+ return Response.json({ url: session.url });
753
+ }
754
+ \`\`\`
755
+
756
+ ### Verify
757
+ - [ ] Customer portal enabled in Stripe dashboard settings
758
+ - [ ] \`return_url\` uses env var
759
+ - [ ] User has \`stripeCustomerId\` before attempting (guard against new users)
760
+ `;
761
+ }
762
+ ;
763
+ // ---------------------------------------------------------------------------
764
+ // Prompts UI, API, Deployment, Email, Analytics, Security
765
+ // ---------------------------------------------------------------------------
766
+ function uiPrompts(stack) {
767
+ return `# UI Prompts
768
+
769
+ ## New Page / Route
770
+
771
+ ### Build
772
+ \`\`\`typescript
773
+ // app/dashboard/posts/page.tsx
774
+ import { auth } from '@clerk/nextjs/server';
775
+ import { redirect } from 'next/navigation';
776
+ import { db } from '@/lib/db';
777
+
778
+ export const metadata = { title: 'Posts | MyApp' };
779
+
780
+ export default async function PostsPage() {
781
+ const { userId } = auth();
782
+ if (!userId) redirect('/sign-in');
783
+
784
+ const posts = await db.post.findMany({
785
+ where: { authorId: userId, deletedAt: null },
786
+ orderBy: { createdAt: 'desc' },
787
+ take: 20,
788
+ });
789
+
790
+ return (
791
+ <div className="container mx-auto py-8">
792
+ <h1 className="text-2xl font-bold mb-6">Posts</h1>
793
+ {posts.length === 0 ? (
794
+ <EmptyState message="No posts yet" action={{ label: 'Create post', href: '/dashboard/posts/new' }} />
795
+ ) : (
796
+ <PostList posts={posts} />
797
+ )}
798
+ </div>
799
+ );
800
+ }
801
+ \`\`\`
802
+
803
+ ### Verify
804
+ - [ ] Auth check at top (before any DB query)
805
+ - [ ] \`metadata\` exported for SEO
806
+ - [ ] Empty state handled
807
+ - [ ] Loading state (add \`loading.tsx\` sibling file if needed)
808
+ - [ ] Error state (add \`error.tsx\` sibling file if needed)
809
+ - [ ] Paginated if list could exceed 20 items
810
+
811
+ ### Debug
812
+ - Page flickers on load → missing Suspense boundary, wrap async component
813
+ - Auth redirecting logged-in users → middleware matcher too broad
814
+
815
+ ---
816
+
817
+ ## Form with Server Action
818
+
819
+ ### Build
820
+ \`\`\`typescript
821
+ // app/dashboard/posts/new/page.tsx + actions/posts.ts
822
+
823
+ // actions/posts.ts
824
+ 'use server';
825
+ import { auth } from '@clerk/nextjs/server';
826
+ import { z } from 'zod';
827
+ import { db } from '@/lib/db';
828
+ import { revalidatePath } from 'next/cache';
829
+
830
+ const CreatePostSchema = z.object({
831
+ title: z.string().min(1).max(200),
832
+ content: z.string().min(1),
833
+ });
834
+
835
+ export async function createPost(formData: FormData) {
836
+ const { userId } = auth();
837
+ if (!userId) return { error: 'Unauthorized' };
838
+
839
+ const raw = { title: formData.get('title'), content: formData.get('content') };
840
+ const parsed = CreatePostSchema.safeParse(raw);
841
+ if (!parsed.success) return { error: parsed.error.flatten().fieldErrors };
842
+
843
+ const post = await db.post.create({
844
+ data: { ...parsed.data, authorId: userId },
845
+ });
846
+
847
+ revalidatePath('/dashboard/posts');
848
+ return { success: true, postId: post.id };
849
+ }
850
+
851
+ // Component
852
+ 'use client';
853
+ import { useTransition } from 'react';
854
+ import { createPost } from '@/actions/posts';
855
+
856
+ export function CreatePostForm() {
857
+ const [isPending, startTransition] = useTransition();
858
+ const [error, setError] = useState<string | null>(null);
859
+
860
+ function handleSubmit(formData: FormData) {
861
+ startTransition(async () => {
862
+ const result = await createPost(formData);
863
+ if (result.error) setError(typeof result.error === 'string' ? result.error : 'Validation failed');
864
+ });
865
+ }
866
+
867
+ return (
868
+ <form action={handleSubmit} className="space-y-4">
869
+ <input name="title" required className="w-full border rounded px-3 py-2" />
870
+ <textarea name="content" required className="w-full border rounded px-3 py-2" />
871
+ {error && <p className="text-red-500 text-sm">{error}</p>}
872
+ <button type="submit" disabled={isPending}>
873
+ {isPending ? 'Creating...' : 'Create Post'}
874
+ </button>
875
+ </form>
876
+ );
877
+ }
878
+ \`\`\`
879
+
880
+ ### Verify
881
+ - [ ] Server Action has auth check at top
882
+ - [ ] Zod validation on server (never trust client-side validation alone)
883
+ - [ ] \`revalidatePath\` called after mutation to refresh cached data
884
+ - [ ] Loading state shown while pending
885
+ - [ ] Error displayed to user (not just console.log)
886
+ - [ ] No \`<form>\` with \`method="post"\` — use Server Actions
887
+
888
+ ---
889
+
890
+ ## Data Table with Pagination
891
+
892
+ ### Build
893
+ \`\`\`typescript
894
+ // Use URL search params for pagination state (shareable, no useState)
895
+ // app/dashboard/posts/page.tsx
896
+
897
+ export default async function PostsPage({ searchParams }: { searchParams: { page?: string } }) {
898
+ const page = Number(searchParams.page ?? 1) - 1;
899
+ const pageSize = 20;
900
+
901
+ const [posts, total] = await Promise.all([
902
+ db.post.findMany({
903
+ where: { deletedAt: null },
904
+ orderBy: { createdAt: 'desc' },
905
+ take: pageSize,
906
+ skip: page * pageSize,
907
+ }),
908
+ db.post.count({ where: { deletedAt: null } }),
909
+ ]);
910
+
911
+ const totalPages = Math.ceil(total / pageSize);
912
+
913
+ return (
914
+ <div>
915
+ <PostTable posts={posts} />
916
+ <Pagination currentPage={page + 1} totalPages={totalPages} />
917
+ </div>
918
+ );
919
+ }
920
+ \`\`\`
921
+
922
+ ### Verify
923
+ - [ ] Pagination uses URL params (not useState) — shareable URLs
924
+ - [ ] Total count fetched in parallel with data (\`Promise.all\`)
925
+ - [ ] Page out of range handled gracefully (redirect to page 1)
926
+ - [ ] Empty state shown when no results
927
+
928
+ ---
929
+
930
+ ## Loading & Error States
931
+
932
+ ### Build
933
+ \`\`\`typescript
934
+ // app/dashboard/posts/loading.tsx — automatic Suspense fallback
935
+ export default function Loading() {
936
+ return <PostTableSkeleton />;
937
+ }
938
+
939
+ // app/dashboard/posts/error.tsx — catches thrown errors
940
+ 'use client';
941
+ export default function Error({ error, reset }: { error: Error; reset: () => void }) {
942
+ return (
943
+ <div className="text-center py-12">
944
+ <p className="text-red-500 mb-4">Something went wrong</p>
945
+ <button onClick={reset}>Try again</button>
946
+ </div>
947
+ );
948
+ }
949
+ \`\`\`
950
+
951
+ ### Verify
952
+ - [ ] Every async page has a \`loading.tsx\` sibling
953
+ - [ ] Every page that fetches data has an \`error.tsx\` sibling
954
+ - [ ] Skeleton matches layout of loaded content (prevents layout shift)
955
+ `;
956
+ }
957
+ ;
958
+ function apiPrompts(stack) {
959
+ return `# API Prompts
960
+
961
+ ## New Route Handler
962
+
963
+ ### Build
964
+ \`\`\`typescript
965
+ // app/api/posts/route.ts
966
+ import { auth } from '@clerk/nextjs/server';
967
+ import { z } from 'zod';
968
+ import { db } from '@/lib/db';
969
+
970
+ const CreatePostSchema = z.object({
971
+ title: z.string().min(1).max(200),
972
+ content: z.string().optional(),
973
+ });
974
+
975
+ export async function POST(req: Request) {
976
+ // 1. Auth
977
+ const { userId } = auth();
978
+ if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
979
+
980
+ // 2. Parse & validate
981
+ let body: unknown;
982
+ try { body = await req.json(); }
983
+ catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }); }
984
+
985
+ const parsed = CreatePostSchema.safeParse(body);
986
+ if (!parsed.success) {
987
+ return Response.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 422 });
988
+ }
989
+
990
+ // 3. Business logic
991
+ const post = await db.post.create({
992
+ data: { ...parsed.data, authorId: userId },
993
+ select: { id: true, title: true, createdAt: true },
994
+ });
995
+
996
+ return Response.json({ data: post }, { status: 201 });
997
+ }
998
+
999
+ export async function GET(req: Request) {
1000
+ const { userId } = auth();
1001
+ if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
1002
+
1003
+ const { searchParams } = new URL(req.url);
1004
+ const page = Number(searchParams.get('page') ?? 1) - 1;
1005
+
1006
+ const posts = await db.post.findMany({
1007
+ where: { authorId: userId, deletedAt: null },
1008
+ orderBy: { createdAt: 'desc' },
1009
+ take: 20,
1010
+ skip: page * 20,
1011
+ select: { id: true, title: true, published: true, createdAt: true },
1012
+ });
1013
+
1014
+ return Response.json({ data: posts });
1015
+ }
1016
+ \`\`\`
1017
+
1018
+ ### Verify
1019
+ - [ ] Auth check is first line (before JSON parsing)
1020
+ - [ ] JSON parse wrapped in try/catch
1021
+ - [ ] Zod validation with specific error response
1022
+ - [ ] Returns \`{ data: T }\` on success, \`{ error: string }\` on failure
1023
+ - [ ] Correct HTTP status codes (201 for create, 200 for get, 422 for validation, 401/403 for auth)
1024
+ - [ ] Only returns fields needed (\`select\`)
1025
+
1026
+ ### Debug
1027
+ - 400 on valid request → JSON parse issue, check Content-Type header sent by client
1028
+ - Zod errors not showing → not returning \`parsed.error.flatten()\` in response
1029
+ - Auth passing but DB query wrong user's data → using \`userId\` from Clerk but querying with DB \`id\` (join on clerkId first)
1030
+ `;
1031
+ }
1032
+ ;
1033
+ function deploymentPrompts(stack) {
1034
+ return `# Deployment Prompts
1035
+
1036
+ ## Add Cron Job (Railway)
1037
+
1038
+ ### Build
1039
+ 1. In Railway dashboard: New Service → Cron
1040
+ 2. Set schedule (standard cron syntax)
1041
+ 3. Set command: \`curl -X GET https://yourapp.com/api/cron/job-name -H "x-cron-secret: $CRON_SECRET"\`
1042
+
1043
+ \`\`\`typescript
1044
+ // app/api/cron/[job]/route.ts
1045
+ export async function GET(req: Request, { params }: { params: { job: string } }) {
1046
+ // Auth
1047
+ const secret = req.headers.get('x-cron-secret');
1048
+ if (secret !== process.env.CRON_SECRET) {
1049
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
1050
+ }
1051
+
1052
+ const startTime = Date.now();
1053
+ console.log(\`[CRON:\${params.job}] Starting\`);
1054
+
1055
+ try {
1056
+ switch (params.job) {
1057
+ case 'send-digests':
1058
+ await sendWeeklyDigests();
1059
+ break;
1060
+ case 'expire-trials':
1061
+ await expireTrials();
1062
+ break;
1063
+ default:
1064
+ return Response.json({ error: 'Unknown job' }, { status: 404 });
1065
+ }
1066
+
1067
+ const duration = Date.now() - startTime;
1068
+ console.log(\`[CRON:\${params.job}] Done in \${duration}ms\`);
1069
+ return Response.json({ success: true, duration });
1070
+ } catch (error) {
1071
+ console.error(\`[CRON:\${params.job}] Failed\`, error);
1072
+ return Response.json({ error: 'Job failed' }, { status: 500 });
1073
+ }
1074
+ }
1075
+ \`\`\`
1076
+
1077
+ ### Verify
1078
+ - [ ] \`CRON_SECRET\` set in Railway env vars
1079
+ - [ ] Job is idempotent (safe to run multiple times)
1080
+ - [ ] Execution time logged
1081
+ - [ ] Error logged with full context
1082
+ - [ ] Route in public routes (not behind auth middleware)
1083
+ - [ ] Job name validated (no default fallthrough)
1084
+
1085
+ ### Debug
1086
+ - 401 on cron → \`CRON_SECRET\` env var not set in Railway, or secret mismatch
1087
+ - Job runs but does nothing → check idempotency logic isn't skipping everything
1088
+ - Timeout → Railway cron has 10min timeout, split large jobs into batches
1089
+
1090
+ ---
1091
+
1092
+ ## New Environment Variable
1093
+
1094
+ ### Build
1095
+ 1. Add to \`.env.example\` (with fake value — never real credentials)
1096
+ 2. Add to \`.env.local\` (real dev value — gitignored)
1097
+ 3. Add to Railway dashboard under service → Variables
1098
+ 4. Add to \`src/env.ts\` validation (if using t3-env or similar)
1099
+
1100
+ \`\`\`typescript
1101
+ // src/env.ts — validate all env vars at startup
1102
+ import { z } from 'zod';
1103
+
1104
+ const envSchema = z.object({
1105
+ DATABASE_URL: z.string().url(),
1106
+ CRON_SECRET: z.string().min(32),
1107
+ RESEND_API_KEY: z.string().startsWith('re_'),
1108
+ STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
1109
+ STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
1110
+ NEXT_PUBLIC_APP_URL: z.string().url(),
1111
+ NEXT_PUBLIC_GA_MEASUREMENT_ID: z.string().startsWith('G-').optional(),
1112
+ });
1113
+
1114
+ export const env = envSchema.parse(process.env);
1115
+ \`\`\`
1116
+
1117
+ ### Verify
1118
+ - [ ] Added to \`.env.example\` with placeholder
1119
+ - [ ] Added to Railway dashboard (staging + prod environments separately if applicable)
1120
+ - [ ] Validated in \`src/env.ts\` startup check
1121
+ - [ ] \`NEXT_PUBLIC_\` prefix only for vars that must be client-accessible
1122
+ - [ ] Never committed real values to git
1123
+
1124
+ ### Debug
1125
+ - Undefined in production → added to \`.env.local\` but not Railway dashboard
1126
+ - Client component getting undefined → missing \`NEXT_PUBLIC_\` prefix
1127
+ `;
1128
+ }
1129
+ ;
1130
+ function emailPrompts(stack) {
1131
+ return `# Email Prompts
1132
+
1133
+ ## Send Transactional Email
1134
+
1135
+ ### Build
1136
+ \`\`\`typescript
1137
+ // src/lib/send-email.ts — wrapper with logging
1138
+ import { resend } from '@/lib/email';
1139
+
1140
+ interface SendEmailOptions {
1141
+ to: string;
1142
+ subject: string;
1143
+ react: React.ReactElement;
1144
+ userId?: string; // for logging
1145
+ type: string; // for logging
1146
+ }
1147
+
1148
+ export async function sendEmail({ to, subject, react, userId, type }: SendEmailOptions) {
1149
+ try {
1150
+ const { data, error } = await resend.emails.send({
1151
+ from: process.env.EMAIL_FROM!,
1152
+ to,
1153
+ subject,
1154
+ react,
1155
+ });
1156
+
1157
+ if (error) {
1158
+ console.error(\`[EMAIL:\${type}] Failed\`, { error, to, userId });
1159
+ return { success: false, error };
1160
+ }
1161
+
1162
+ console.log(\`[EMAIL:\${type}] Sent\`, { id: data?.id, to, userId });
1163
+ return { success: true, id: data?.id };
1164
+ } catch (err) {
1165
+ console.error(\`[EMAIL:\${type}] Exception\`, { err, to, userId });
1166
+ return { success: false, error: err };
1167
+ }
1168
+ }
1169
+
1170
+ // Usage — never block the main flow on email
1171
+ const emailResult = await sendEmail({
1172
+ to: user.email,
1173
+ subject: 'Welcome!',
1174
+ react: <WelcomeEmail name={user.name} />,
1175
+ userId: user.id,
1176
+ type: 'welcome',
1177
+ });
1178
+ // Continue even if email fails — log but don't throw
1179
+ \`\`\`
1180
+
1181
+ ### Verify
1182
+ - [ ] Called from server only (Server Action, API route, webhook handler)
1183
+ - [ ] Never blocks the user-facing response — fire after main operation succeeds
1184
+ - [ ] Errors logged with context but not thrown
1185
+ - [ ] \`EMAIL_FROM\` uses env var (domain verified in Resend)
1186
+ - [ ] \`type\` field used for log filtering
1187
+
1188
+ ### Debug
1189
+ - Emails not arriving → check Resend dashboard logs, verify domain DNS records
1190
+ - "From address not verified" → domain not verified in Resend, check DNS TXT/DKIM records
1191
+ - Email in spam → missing DKIM records, check Cloudflare DNS (must be grey cloud, not proxied)
1192
+
1193
+ ---
1194
+
1195
+ ## New Email Template
1196
+
1197
+ ### Build
1198
+ \`\`\`typescript
1199
+ // src/emails/payment-failed.tsx
1200
+ import {
1201
+ Html, Head, Body, Container, Heading,
1202
+ Text, Button, Hr
1203
+ } from '@react-email/components';
1204
+
1205
+ interface PaymentFailedEmailProps {
1206
+ userName: string;
1207
+ updatePaymentUrl: string;
1208
+ }
1209
+
1210
+ export function PaymentFailedEmail({ userName, updatePaymentUrl }: PaymentFailedEmailProps) {
1211
+ return (
1212
+ <Html>
1213
+ <Head />
1214
+ <Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f9f9f9' }}>
1215
+ <Container style={{ maxWidth: '560px', margin: '0 auto', padding: '40px 20px' }}>
1216
+ <Heading>Payment failed</Heading>
1217
+ <Text>Hi {userName},</Text>
1218
+ <Text>
1219
+ We couldn't process your payment. Please update your payment method
1220
+ to continue using the service.
1221
+ </Text>
1222
+ <Button href={updatePaymentUrl} style={{ backgroundColor: '#000', color: '#fff', padding: '12px 24px' }}>
1223
+ Update payment method
1224
+ </Button>
1225
+ <Hr />
1226
+ <Text style={{ fontSize: '12px', color: '#666' }}>
1227
+ If you have questions, reply to this email.
1228
+ </Text>
1229
+ </Container>
1230
+ </Body>
1231
+ </Html>
1232
+ );
1233
+ }
1234
+ \`\`\`
1235
+
1236
+ ### Verify
1237
+ - [ ] Props are typed (no \`any\`)
1238
+ - [ ] Preview with \`email.react\` package: \`pnpm email dev\`
1239
+ - [ ] Mobile-friendly (single column, large tap targets)
1240
+ - [ ] Fallback plain text (Resend handles this automatically)
1241
+ - [ ] No tracking pixels unless consent obtained
1242
+ `;
1243
+ }
1244
+ ;
1245
+ function analyticsPrompts(stack) {
1246
+ return `# Analytics Prompts
1247
+
1248
+ ## Track Custom Event (GA4)
1249
+
1250
+ ### Build
1251
+ \`\`\`typescript
1252
+ // src/lib/analytics.ts
1253
+ export function trackEvent(event: {
1254
+ action: string;
1255
+ category: string;
1256
+ label?: string;
1257
+ value?: number;
1258
+ }) {
1259
+ if (typeof window === 'undefined') return;
1260
+ if (!process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID) return;
1261
+
1262
+ window.gtag('event', event.action, {
1263
+ event_category: event.category,
1264
+ event_label: event.label,
1265
+ value: event.value,
1266
+ });
1267
+ }
1268
+
1269
+ // Standard events — use these names for GA4 reports
1270
+ export const events = {
1271
+ signUp: (method: string) => trackEvent({ action: 'sign_up', category: 'auth', label: method }),
1272
+ login: (method: string) => trackEvent({ action: 'login', category: 'auth', label: method }),
1273
+ beginCheckout: (plan: string) => trackEvent({ action: 'begin_checkout', category: 'ecommerce', label: plan }),
1274
+ purchase: (plan: string, value: number) => trackEvent({ action: 'purchase', category: 'ecommerce', label: plan, value }),
1275
+ featureUsed: (feature: string) => trackEvent({ action: 'feature_used', category: 'engagement', label: feature }),
1276
+ cancelSubscription: () => trackEvent({ action: 'cancel_subscription', category: 'billing' }),
1277
+ };
1278
+ \`\`\`
1279
+
1280
+ ### Verify
1281
+ - [ ] \`typeof window !== 'undefined'\` guard (runs in Server Components too)
1282
+ - [ ] Event names match GA4 recommended event names where possible
1283
+ - [ ] No PII in event labels (no emails, names, IDs)
1284
+ - [ ] Only fires if GA Measurement ID is present (safe in preview/dev)
1285
+
1286
+ ### Debug
1287
+ - Events not showing in GA4 → check real-time report in GA4 (takes up to 24h for standard reports)
1288
+ - Blocked by ad blocker → use GA4 Measurement Protocol for server-side events instead
1289
+ - \`gtag is not defined\` → GA script not loaded, check layout.tsx Script tags
1290
+ `;
1291
+ }
1292
+ ;
1293
+ function securityPrompts(stack) {
1294
+ return `# Security Prompts
1295
+
1296
+ ## Input Validation (All Entry Points)
1297
+
1298
+ ### Build
1299
+ \`\`\`typescript
1300
+ // Every API route and Server Action validates ALL inputs with Zod
1301
+ // Never trust: req.body, req.query, req.params, formData, searchParams
1302
+
1303
+ import { z } from 'zod';
1304
+
1305
+ // Strict schemas — reject anything unexpected
1306
+ const schema = z.object({
1307
+ email: z.string().email().toLowerCase(),
1308
+ name: z.string().min(1).max(100).trim(),
1309
+ amount: z.number().int().positive().max(100_000), // cents
1310
+ role: z.enum(['user', 'viewer']), // never accept 'admin' from input
1311
+ });
1312
+
1313
+ // File uploads
1314
+ const fileSchema = z.object({
1315
+ size: z.number().max(5 * 1024 * 1024), // 5MB max
1316
+ type: z.enum(['image/jpeg', 'image/png', 'image/webp']),
1317
+ });
1318
+ \`\`\`
1319
+
1320
+ ### Verify
1321
+ - [ ] All API routes validate with Zod
1322
+ - [ ] Roles can never be escalated via input (never accept \`role: 'admin'\` from request)
1323
+ - [ ] String lengths bounded (prevents oversized payload attacks)
1324
+ - [ ] Email lowercased and trimmed
1325
+ - [ ] File uploads: size + mime type validated server-side
1326
+
1327
+ ---
1328
+
1329
+ ## Secure Direct Object References
1330
+
1331
+ ### Build
1332
+ \`\`\`typescript
1333
+ // ALWAYS scope queries to the current user's data
1334
+ // Never trust a resource ID without verifying ownership
1335
+
1336
+ // ❌ Wrong — IDOR vulnerability
1337
+ const post = await db.post.findUnique({ where: { id: postId } });
1338
+
1339
+ // ✅ Correct — always include userId in where clause
1340
+ const post = await db.post.findUnique({
1341
+ where: { id: postId, authorId: userId }, // fails if not owner
1342
+ });
1343
+
1344
+ if (!post) return Response.json({ error: 'Not found' }, { status: 404 });
1345
+ // Note: return 404, not 403 — don't reveal resource existence
1346
+ \`\`\`
1347
+
1348
+ ### Verify
1349
+ - [ ] Every query for user-owned data includes \`userId\` in where clause
1350
+ - [ ] Returns 404 (not 403) when resource not found or not owned
1351
+ - [ ] Admin endpoints have separate admin-only middleware
1352
+
1353
+ ---
1354
+
1355
+ ## Rate Limiting
1356
+
1357
+ ### Build
1358
+ \`\`\`typescript
1359
+ // Use Cloudflare for coarse rate limiting (set in dashboard)
1360
+ // Use in-memory or Upstash Redis for fine-grained app-level limits
1361
+
1362
+ // Simple in-memory rate limiter (good for low traffic)
1363
+ const rateLimit = new Map<string, { count: number; resetAt: number }>();
1364
+
1365
+ export function checkRateLimit(key: string, limit: number, windowMs: number): boolean {
1366
+ const now = Date.now();
1367
+ const entry = rateLimit.get(key);
1368
+
1369
+ if (!entry || now > entry.resetAt) {
1370
+ rateLimit.set(key, { count: 1, resetAt: now + windowMs });
1371
+ return true; // allowed
1372
+ }
1373
+
1374
+ if (entry.count >= limit) return false; // blocked
1375
+
1376
+ entry.count++;
1377
+ return true; // allowed
1378
+ }
1379
+
1380
+ // Usage in API route
1381
+ const allowed = checkRateLimit(\`auth:\${ip}\`, 5, 60_000); // 5 per minute
1382
+ if (!allowed) {
1383
+ return Response.json({ error: 'Too many requests' }, { status: 429 });
1384
+ }
1385
+ \`\`\`
1386
+
1387
+ ### Verify
1388
+ - [ ] Auth endpoints rate limited (sign-in, magic link, password reset)
1389
+ - [ ] Rate limit keyed by IP (for unauthed) or userId (for authed)
1390
+ - [ ] 429 returned with \`Retry-After\` header when possible
1391
+ - [ ] Cloudflare WAF rules set for aggressive bots (see cloudflare.md)
1392
+ `;
1393
+ }
1394
+ ;
1395
+ // ---------------------------------------------------------------------------
1396
+ // Skills
1397
+ // ---------------------------------------------------------------------------
1398
+ function frontendSkill() {
1399
+ return `# SKILL: Frontend Engineer
1400
+
1401
+ > Load this at the start of a UI-heavy session.
1402
+ > Tell Claude: "Enter frontend mode" or "Load /ai-docs/skills/frontend.md"
1403
+
1404
+ ## Persona
1405
+ You are a senior frontend engineer. You think in components, user experience, and performance. You write clean, accessible, maintainable React code.
1406
+
1407
+ ## Priorities (in order)
1408
+ 1. **Correctness** — does it work for all states (loading, error, empty, populated)?
1409
+ 2. **Accessibility** — keyboard nav, ARIA labels, focus management
1410
+ 3. **Performance** — no unnecessary re-renders, lazy load heavy components
1411
+ 4. **Simplicity** — fewest components needed, no over-engineering
1412
+
1413
+ ## Defaults
1414
+ - Server Components unless you need client interactivity
1415
+ - Tailwind for all styling — never inline styles, never CSS modules
1416
+ - \`cn()\` (clsx + tailwind-merge) for conditional classes
1417
+ - \`useTransition\` for Server Actions (not \`useState\` + \`useEffect\` workarounds)
1418
+ - URL state for filters/pagination (not useState)
1419
+ - Zod validation in Server Actions — not just client-side
1420
+
1421
+ ## Component Checklist (before finishing)
1422
+ - [ ] Loading state handled
1423
+ - [ ] Error state handled
1424
+ - [ ] Empty state handled
1425
+ - [ ] Mobile responsive (test at 375px)
1426
+ - [ ] Keyboard navigable
1427
+ - [ ] No hardcoded colors or spacing — Tailwind tokens only
1428
+ - [ ] TypeScript types complete (no \`any\`)
1429
+
1430
+ ## Common Mistakes to Avoid
1431
+ - \`useEffect\` to fetch data → use Server Component or React Query instead
1432
+ - useState for URL-based state → use searchParams
1433
+ - Prop drilling more than 2 levels → use context or Zustand
1434
+ - \`className\` string concatenation → use \`cn()\`
1435
+ - Missing \`key\` prop on lists
1436
+ - onClick on div → use button for accessibility
1437
+
1438
+ ## Token-Saving Mode
1439
+ When working on UI tasks:
1440
+ - Write the component first, then ask if you want tests
1441
+ - Don't explain every Tailwind class — just write them
1442
+ - Skip boilerplate comments unless asked
1443
+ - If the pattern is in /ai-docs/prompts/ui.md, follow it exactly — don't improvise
1444
+ `;
1445
+ }
1446
+ function backendSkill() {
1447
+ return `# SKILL: Backend Engineer
1448
+
1449
+ > Load this for API routes, database work, Server Actions, background jobs.
1450
+ > Tell Claude: "Enter backend mode" or "Load /ai-docs/skills/backend.md"
1451
+
1452
+ ## Persona
1453
+ You are a senior backend engineer. You think in data flow, correctness, and reliability. You write APIs that are secure by default, handle errors explicitly, and never trust user input.
1454
+
1455
+ ## Priorities (in order)
1456
+ 1. **Security** — auth checked, input validated, ownership verified
1457
+ 2. **Correctness** — handles all error states, atomic writes, idempotent operations
1458
+ 3. **Performance** — indexed queries, paginated results, no N+1
1459
+ 4. **Simplicity** — no premature abstraction, no over-engineering
1460
+
1461
+ ## Defaults
1462
+ - Validate everything with Zod — API routes AND Server Actions
1463
+ - Auth check is ALWAYS the first line of any handler
1464
+ - Transactions for multi-step writes
1465
+ - Soft deletes (deletedAt) for user data
1466
+ - Always paginate — never unbounded queries
1467
+ - Structured logging: \`console.error('[CONTEXT]', { userId, operation, error })\`
1468
+
1469
+ ## Response Shape (always consistent)
1470
+ \`\`\`typescript
1471
+ // Success
1472
+ { data: T, error: null } // or just Response.json({ data: T })
1473
+
1474
+ // Error
1475
+ { data: null, error: { message: string, code: string } }
1476
+ \`\`\`
1477
+
1478
+ ## HTTP Status Codes
1479
+ - 200: success (GET, PUT, PATCH)
1480
+ - 201: created (POST)
1481
+ - 400: bad request (malformed JSON)
1482
+ - 401: unauthenticated (no session)
1483
+ - 403: unauthorized (wrong permissions / not owner)
1484
+ - 404: not found (use this even for "forbidden" to avoid IDOR)
1485
+ - 422: validation error (Zod failed)
1486
+ - 429: rate limited
1487
+ - 500: server error
1488
+
1489
+ ## Security Checklist (every endpoint)
1490
+ - [ ] Auth checked before any DB access
1491
+ - [ ] Input validated with Zod
1492
+ - [ ] Resource ownership verified (userId in where clause)
1493
+ - [ ] Returns 404 (not 403) for not-owned resources
1494
+
1495
+ ## Token-Saving Mode
1496
+ - Write the handler, then verify checklist items
1497
+ - Don't explain what each Zod field does
1498
+ - Follow /ai-docs/prompts/api.md patterns exactly
1499
+ - If it's a webhook, check /ai-docs/prompts/payments.md first
1500
+ `;
1501
+ }
1502
+ function devopsSkill() {
1503
+ return `# SKILL: DevOps / Infrastructure
1504
+
1505
+ > Load this for deployment, env vars, cron jobs, Railway config, Cloudflare.
1506
+ > Tell Claude: "Enter devops mode" or "Load /ai-docs/skills/devops.md"
1507
+
1508
+ ## Persona
1509
+ You are a senior DevOps engineer. You think in reliability, observability, and zero-downtime deploys. You treat configuration as code.
1510
+
1511
+ ## Stack
1512
+ - **Hosting:** Railway (web + postgres + cron)
1513
+ - **CDN/DNS:** Cloudflare
1514
+ - **Email:** Resend (transactional)
1515
+ - **Analytics:** GA4 + Google Search Console
1516
+
1517
+ ## Deployment Principles
1518
+ - Every env var validated at startup (fail fast, not silently)
1519
+ - Migrations run before the app starts (not after)
1520
+ - Health check at \`/api/health\` always available
1521
+ - Never store secrets in code or logs
1522
+
1523
+ ## Railway Checklist
1524
+ - [ ] \`DATABASE_URL\` linked from Railway PostgreSQL plugin (not hardcoded)
1525
+ - [ ] Start command includes migrations: \`pnpm db:migrate && pnpm start\`
1526
+ - [ ] Health check configured in Railway service settings
1527
+ - [ ] Custom domain added + SSL auto-generated
1528
+ - [ ] Cloudflare DNS pointing to Railway URL (proxied)
1529
+
1530
+ ## Cloudflare Checklist
1531
+ - [ ] SSL mode: Full (strict)
1532
+ - [ ] Cache rules set (static assets cached, /api/* bypassed)
1533
+ - [ ] Mail DNS records NOT proxied (Resend DKIM breaks behind Cloudflare proxy)
1534
+ - [ ] Security rules: rate limit /api/auth/signin
1535
+
1536
+ ## Cron Job Checklist
1537
+ - [ ] CRON_SECRET set (min 32 chars)
1538
+ - [ ] Job is idempotent
1539
+ - [ ] Job has timeout handling (Railway max: 10 min)
1540
+ - [ ] Job logs start time + duration
1541
+ - [ ] Railway cron service pointed at correct URL
1542
+
1543
+ ## Env Var Naming Convention
1544
+ \`\`\`
1545
+ DATABASE_URL # always
1546
+ CRON_SECRET # for cron auth
1547
+ STRIPE_SECRET_KEY # sk_test_... / sk_live_...
1548
+ STRIPE_WEBHOOK_SECRET # whsec_...
1549
+ RESEND_API_KEY # re_...
1550
+ CLERK_SECRET_KEY # sk_...
1551
+ CLERK_WEBHOOK_SECRET # whsec_...
1552
+ NEXT_PUBLIC_APP_URL # https://yourapp.com
1553
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY # pk_...
1554
+ NEXT_PUBLIC_GA_MEASUREMENT_ID # G-XXXXXXX
1555
+ CLOUDFLARE_ZONE_ID # for cache purge
1556
+ CLOUDFLARE_API_TOKEN # scoped to cache purge only
1557
+ \`\`\`
1558
+
1559
+ ## Token-Saving Mode
1560
+ - Follow /ai-docs/deployment.md exactly
1561
+ - Don't explain Railway's UI — give the config values
1562
+ - List checklist items, don't prose-explain each one
1563
+ `;
1564
+ }
1565
+ function reviewerSkill() {
1566
+ return `# SKILL: Code Reviewer
1567
+
1568
+ > Load this when you want Claude to critically review code before shipping.
1569
+ > Tell Claude: "Review this as a senior engineer" or "Load /ai-docs/skills/reviewer.md"
1570
+
1571
+ ## Persona
1572
+ You are a principal engineer doing a pre-ship code review. You are thorough, specific, and constructive. You catch what junior engineers miss.
1573
+
1574
+ ## Review Order
1575
+ 1. **Security holes** — auth missing, IDOR, unvalidated input, secret exposure
1576
+ 2. **Correctness** — edge cases, error handling, race conditions
1577
+ 3. **Performance** — N+1 queries, missing indexes, unbounded queries
1578
+ 4. **Maintainability** — naming, complexity, duplication
1579
+ 5. **Style** — only if everything above is clean
1580
+
1581
+ ## Security Red Flags (always call out)
1582
+ - Resource fetched without scoping to userId
1583
+ - Missing auth check in API route or Server Action
1584
+ - \`any\` type hiding a validation gap
1585
+ - User input used in DB query without Zod first
1586
+ - Secrets hardcoded or logged
1587
+
1588
+ ## Correctness Red Flags
1589
+ - Missing loading/error/empty states
1590
+ - \`useEffect\` with missing dependencies
1591
+ - Mutation without \`revalidatePath\` (stale cache)
1592
+ - Webhook without idempotency check
1593
+ - Multi-step write without transaction
1594
+
1595
+ ## Output Format
1596
+ For each issue:
1597
+ \`\`\`
1598
+ 🔴 CRITICAL (Security/Data loss): [issue] → [fix]
1599
+ 🟡 WARNING (Correctness/Performance): [issue] → [fix]
1600
+ 🟢 SUGGESTION (Style/Maintainability): [issue] → [fix]
1601
+ \`\`\`
1602
+
1603
+ Only show 🟢 if there are no 🔴 or 🟡 issues.
1604
+ Stop at 🔴 — fix security issues before reviewing anything else.
1605
+
1606
+ ## Token-Saving Mode
1607
+ - Lead with the most critical issue
1608
+ - Don't compliment the code
1609
+ - Don't explain why security matters — just state the issue and fix
1610
+ - If code is clean, say "Looks good — no issues found" and stop
1611
+ `;
1612
+ }
1613
+ // ---------------------------------------------------------------------------
1614
+ // Extras (Security, Handover, Mistakes, Token Rules)
1615
+ // ---------------------------------------------------------------------------
1616
+ function securityDoc(stack) {
1617
+ return `# Security
1618
+
1619
+ ## OWASP Top 10 — Applied to This Stack
1620
+
1621
+ ### A01 Broken Access Control
1622
+ **Risk:** User accesses another user's data by guessing IDs
1623
+ **Protection:**
1624
+ - Always include \`userId\` in DB queries for user-owned data
1625
+ - Return 404 (not 403) when resource not owned — don't reveal existence
1626
+ - Admin routes protected by role check, not just auth check
1627
+ - Test: log in as user A, try to access user B's resource by ID
1628
+
1629
+ ### A02 Cryptographic Failures
1630
+ **Risk:** Sensitive data exposed in logs, responses, or storage
1631
+ **Protection:**
1632
+ - Never log: passwords, tokens, full credit card numbers, SSNs
1633
+ - Never return: password hashes, internal IDs, raw Stripe keys
1634
+ - DB connection over SSL (Railway enforces this)
1635
+ - Env vars for all secrets — never in code
1636
+
1637
+ ### A03 Injection
1638
+ **Risk:** SQL injection via unsanitized input
1639
+ **Protection:** Prisma/Drizzle parameterize all queries automatically
1640
+ **Watch out for:** Raw SQL with template literals — use \`prisma.$queryRaw\` tag safely:
1641
+ \`\`\`typescript
1642
+ // ❌ Vulnerable
1643
+ prisma.$queryRawUnsafe(\`SELECT * FROM users WHERE id = '\${userId}'\`);
1644
+
1645
+ // ✅ Safe
1646
+ prisma.$queryRaw\`SELECT * FROM users WHERE id = \${userId}\`;
1647
+ \`\`\`
1648
+
1649
+ ### A04 Insecure Design
1650
+ **Risk:** Business logic flaws (negative balances, skipping payment, etc.)
1651
+ **Protection:**
1652
+ - Validate business rules server-side: check credits before spending, check subscription before access
1653
+ - Use transactions for multi-step operations (can't spend credits without creating the job)
1654
+ - Never trust client on anything financial
1655
+
1656
+ ### A05 Security Misconfiguration
1657
+ **Risk:** Debug info exposed, unnecessary features enabled
1658
+ **Checklist:**
1659
+ - [ ] \`NODE_ENV=production\` in Railway
1660
+ - [ ] No \`.env\` files committed
1661
+ - [ ] Error responses don't expose stack traces (check production error responses)
1662
+ - [ ] Stripe in live mode (not test mode) in production
1663
+ - [ ] Clerk in production instance (not development)
1664
+
1665
+ ### A07 Identification & Authentication Failures
1666
+ **Risk:** Brute force, session fixation, credential stuffing
1667
+ **Protection:**
1668
+ - Clerk handles session management (don't roll your own)
1669
+ - Rate limit auth endpoints: 5 attempts per minute per IP (Cloudflare rule)
1670
+ - Magic link / OTP expiry: 15 minutes max
1671
+ - Never store session tokens in localStorage
1672
+
1673
+ ### A08 Software & Data Integrity
1674
+ **Risk:** Supply chain attacks via npm packages
1675
+ **Protection:**
1676
+ - Lock file committed (\`pnpm-lock.yaml\`)
1677
+ - Stripe webhook signature verified before processing
1678
+ - Clerk webhook (Svix) signature verified before processing
1679
+
1680
+ ### A09 Security Logging & Monitoring
1681
+ **What to log:**
1682
+ \`\`\`typescript
1683
+ // Auth events
1684
+ console.log('[AUTH]', { event: 'sign_in', userId, ip, method });
1685
+ console.error('[AUTH_FAIL]', { event: 'invalid_token', ip });
1686
+
1687
+ // Financial events
1688
+ console.log('[BILLING]', { event: 'subscription_created', userId, plan, stripeSubId });
1689
+ console.error('[BILLING_FAIL]', { event: 'payment_failed', userId, invoiceId });
1690
+
1691
+ // Security events
1692
+ console.error('[SECURITY]', { event: 'invalid_webhook_sig', source: 'stripe', ip });
1693
+ console.error('[SECURITY]', { event: 'rate_limit_exceeded', ip, endpoint });
1694
+ \`\`\`
1695
+
1696
+ ---
1697
+
1698
+ ## STRIDE Threat Model
1699
+
1700
+ | Threat | Example | Mitigation |
1701
+ |--------|---------|------------|
1702
+ | **S**poofing | Faking another user's session | Clerk session management, verify userId server-side |
1703
+ | **T**ampering | Modifying request body to escalate role | Zod validation, never accept role from input |
1704
+ | **R**epudiation | User denies making a purchase | Stripe webhook log + DB WebhookEvent table |
1705
+ | **I**nfo Disclosure | Error leaks stack trace or DB schema | Generic error messages in production |
1706
+ | **D**oS | Flood API with requests | Cloudflare rate limiting + app-level rate limiter |
1707
+ | **E**levation | User accesses admin endpoints | Role check in admin middleware, not just auth |
1708
+
1709
+ ---
1710
+
1711
+ ## Pre-Ship Security Checklist
1712
+
1713
+ ### Authentication
1714
+ - [ ] All protected routes behind auth middleware
1715
+ - [ ] Webhook endpoints excluded from auth middleware
1716
+ - [ ] No auth secrets in client-side code (\`NEXT_PUBLIC_\`)
1717
+
1718
+ ### Authorization
1719
+ - [ ] Every DB query for user data scoped to \`userId\`
1720
+ - [ ] Admin-only operations check \`role === 'admin'\`
1721
+ - [ ] Resource not found = 404, not 403
1722
+
1723
+ ### Input
1724
+ - [ ] All API routes validate with Zod
1725
+ - [ ] All Server Actions validate with Zod
1726
+ - [ ] File uploads: size + mime type validated
1727
+
1728
+ ### Financial
1729
+ - [ ] Stripe webhook signature verified
1730
+ - [ ] Checkout session created server-side only
1731
+ - [ ] Subscription status checked from DB, never client
1732
+
1733
+ ### Secrets
1734
+ - [ ] \`.env\` in \`.gitignore\`
1735
+ - [ ] No secrets in client bundle
1736
+ - [ ] All secrets in Railway env vars
1737
+ `;
1738
+ }
1739
+ function handoverTemplate() {
1740
+ return `# HANDOVER.md — Session Handover
1741
+
1742
+ > Claude writes this at the end of a long session (or when asked).
1743
+ > The next session loads this file to resume instantly without re-exploration.
1744
+ > Command: "Write a handover doc for this session"
1745
+
1746
+ ---
1747
+
1748
+ ## Session Date
1749
+ [Date]
1750
+
1751
+ ## What We Were Building
1752
+ [1-2 sentence description of the feature/fix being worked on]
1753
+
1754
+ ## Current Status
1755
+ - [ ] **In progress** / **Completed** / **Blocked**
1756
+ - [What is done]
1757
+ - [What is NOT done yet]
1758
+
1759
+ ## Files Changed This Session
1760
+ \`\`\`
1761
+ src/app/dashboard/posts/page.tsx - Added pagination
1762
+ src/actions/posts.ts - Added createPost Server Action
1763
+ src/components/PostList.tsx - New component
1764
+ prisma/schema.prisma - Added Post model
1765
+ \`\`\`
1766
+
1767
+ ## Next Steps (in order)
1768
+ 1. [First thing to do next session]
1769
+ 2. [Second thing]
1770
+ 3. [Third thing]
1771
+
1772
+ ## Decisions Made
1773
+ - [Decision]: [Reasoning] — e.g. "Used Server Actions over API routes: simpler, fewer files"
1774
+ - [Decision]: [Reasoning]
1775
+
1776
+ ## Blockers / Questions
1777
+ - [Any unresolved question or blocker]
1778
+
1779
+ ## Context to Load Next Session
1780
+ \`\`\`
1781
+ Read CLAUDE.md
1782
+ Read ai-docs/[relevant domain].md
1783
+ Read ai-docs/prompts/[relevant category].md
1784
+ Read HANDOVER.md
1785
+ Then continue from: [exact next step]
1786
+ \`\`\`
1787
+
1788
+ ---
1789
+ *Tip: Start next session with "Load HANDOVER.md and continue where we left off"*
1790
+ `;
1791
+ }
1792
+ function mistakesTemplate() {
1793
+ return `# MISTAKES.md — Learn From These
1794
+
1795
+ > Before starting any task, check if it's listed here.
1796
+ > Claude: if you're about to do something in this list, stop and do it the right way instead.
1797
+ > Add new mistakes here when they're discovered.
1798
+
1799
+ ## How to Add an Entry
1800
+ When Claude makes a mistake, add:
1801
+ \`\`\`
1802
+ ### [Short title]
1803
+ **Mistake:** What was done wrong
1804
+ **Why it fails:** What breaks or why it's wrong
1805
+ **Correct approach:** What to do instead
1806
+ \`\`\`
1807
+
1808
+ ---
1809
+
1810
+ ## Auth
1811
+
1812
+ ### Using req.json() in Stripe Webhook Handler
1813
+ **Mistake:** \`const body = await req.json()\`
1814
+ **Why it fails:** Parses body before signature verification, destroys raw body — always 400
1815
+ **Correct approach:** \`const body = await req.text()\` — then verify signature, then parse
1816
+
1817
+ ### Redirecting API Routes Instead of Returning 401
1818
+ **Mistake:** \`redirect('/sign-in')\` in an API route handler
1819
+ **Why it fails:** Client gets a redirect response, not JSON — breaks fetch() callers
1820
+ **Correct approach:** \`return Response.json({ error: 'Unauthorized' }, { status: 401 })\`
1821
+
1822
+ ---
1823
+
1824
+ ## Database
1825
+
1826
+ ### Forgetting to Run prisma generate After Schema Change
1827
+ **Mistake:** Changing \`schema.prisma\` and running the app without regenerating client
1828
+ **Why it fails:** TypeScript types don't match new schema, runtime errors on new fields
1829
+ **Correct approach:** Always run \`pnpm prisma generate\` after any schema change
1830
+
1831
+ ### Unbounded Queries
1832
+ **Mistake:** \`db.post.findMany()\` with no \`take\` limit
1833
+ **Why it fails:** Returns all rows — kills performance, high DB cost
1834
+ **Correct approach:** Always add \`take: 20\` and pagination
1835
+
1836
+ ---
1837
+
1838
+ ## Payments
1839
+
1840
+ ### Wrong Webhook Secret
1841
+ **Mistake:** Using Stripe API key as webhook secret
1842
+ **Why it fails:** \`sk_...\` is the API key — webhook secret is \`whsec_...\` from the webhook endpoint page
1843
+ **Correct approach:** Get the secret from Stripe Dashboard → Webhooks → your endpoint → Signing secret
1844
+
1845
+ ---
1846
+
1847
+ ## Deployment
1848
+
1849
+ ### Adding Env Var to .env.local But Not Railway
1850
+ **Mistake:** Works locally, undefined in production
1851
+ **Why it fails:** Railway has its own env var store, separate from local files
1852
+ **Correct approach:** Add to Railway dashboard (service → Variables) AND .env.example
1853
+
1854
+ ---
1855
+
1856
+ ## Next.js
1857
+
1858
+ ### Calling currentUser() When Only userId Is Needed
1859
+ **Mistake:** \`const user = await currentUser()\` just to get the ID
1860
+ **Why it fails:** Makes an extra network request to Clerk on every request — wastes time + money
1861
+ **Correct approach:** \`const { userId } = auth()\` — only call \`currentUser()\` when you need name/email/etc.
1862
+
1863
+ ### Missing revalidatePath After Mutation
1864
+ **Mistake:** Server Action mutates DB but doesn't call \`revalidatePath\`
1865
+ **Why it fails:** Next.js cache not invalidated — UI shows stale data
1866
+ **Correct approach:** Always call \`revalidatePath('/relevant/path')\` at end of Server Action
1867
+ `;
1868
+ }
1869
+ function tokenRulesDoc() {
1870
+ return `# TOKEN-RULES.md — Stay Lean
1871
+
1872
+ > Claude reads this at the start of sessions to minimize token waste.
1873
+ > These rules override default verbosity.
1874
+
1875
+ ## Core Rule
1876
+ **Do less. Say less. Ask before adding.**
1877
+
1878
+ Every unnecessary token costs money and eats into rate limits.
1879
+ Follow these rules every session.
1880
+
1881
+ ---
1882
+
1883
+ ## Response Rules
1884
+
1885
+ ### Be Terse
1886
+ - Answer the question asked — nothing more
1887
+ - Skip preamble: "Great question!" / "Sure, I can help with that" — just start
1888
+ - Skip postamble: "Let me know if you need anything else!" — just stop
1889
+ - Skip explaining obvious things: don't explain what Tailwind does, what TypeScript is
1890
+
1891
+ ### Code First
1892
+ - Write code immediately, explain only what's non-obvious
1893
+ - Don't describe what you're about to write — just write it
1894
+ - Don't re-show code that hasn't changed
1895
+
1896
+ ### Don't Gold-Plate
1897
+ - Build exactly what was asked — no "bonus" features
1898
+ - Don't add error handling that wasn't asked for (but do add it if it's critical)
1899
+ - Don't refactor adjacent code that wasn't mentioned
1900
+ - Don't add tests unless asked
1901
+
1902
+ ### Don't Re-Explain the Codebase
1903
+ - CLAUDE.md was already loaded — don't re-describe the stack
1904
+ - If you've already established a pattern, follow it silently
1905
+ - Don't say "as we discussed" — just use the established pattern
1906
+
1907
+ ---
1908
+
1909
+ ## Session Rules
1910
+
1911
+ ### Load Context Surgically
1912
+ - Load ONE domain doc at a time — only what the current task needs
1913
+ - Don't load all of /ai-docs/ at session start — load on demand
1914
+ - After finishing a task domain, you can stop referencing that doc
1915
+
1916
+ ### Check Before Exploring
1917
+ - Before reading multiple files to understand the codebase, ask: "Which file handles X?"
1918
+ - Don't grep the entire repo when you can ask
1919
+ - Don't read a file "just in case" — only read files directly relevant to the task
1920
+
1921
+ ### When Stuck, Ask — Don't Spiral
1922
+ - If uncertain about an approach, ask one clarifying question
1923
+ - Don't write 3 alternative implementations and ask which to use
1924
+ - Don't explore 5 files hunting for context — ask where to look
1925
+
1926
+ ---
1927
+
1928
+ ## Mistakes That Waste Tokens
1929
+
1930
+ | Mistake | Cost | Fix |
1931
+ |---------|------|-----|
1932
+ | Re-reading CLAUDE.md mid-session | ~500 tokens | Trust the loaded context |
1933
+ | Explaining the stack after it's established | ~200 tokens | Skip it |
1934
+ | Writing 3 versions of the same code | 3x tokens | Write one, confirm approach first if unsure |
1935
+ | Exploring unrelated files | Variable | Ask which file to look at |
1936
+ | Long error explanations | ~300 tokens | State error + fix, skip the lecture |
1937
+ | Adding unasked features | Variable | Ask before adding |
1938
+
1939
+ ---
1940
+
1941
+ ## When to Ask vs Just Do
1942
+
1943
+ **Just do it (no asking needed):**
1944
+ - Task is unambiguous
1945
+ - Pattern exists in /ai-docs/prompts/
1946
+ - You've done this before in the session
1947
+
1948
+ **Ask first (one question max):**
1949
+ - Two valid approaches with different tradeoffs
1950
+ - The task scope is unclear
1951
+ - A decision will be hard to reverse
1952
+
1953
+ **Never ask:**
1954
+ - "Should I add comments?" — no, unless the code is complex
1955
+ - "Want me to add tests?" — only if testing was mentioned
1956
+ - "Would you like me to refactor?" — only if asked
1957
+ `;
1958
+ }
1959
+ // ---------------------------------------------------------------------------
1960
+ // Domain Doc Templates
1961
+ // ---------------------------------------------------------------------------
1962
+ function claudeMd(stack, answers) {
1963
+ const pm = stack.packageManager || 'npm';
1964
+ const run = (script) => pm === 'npm' ? `npm run ${script}` : `${pm} ${script}`;
1965
+ const lines = [];
1966
+ lines.push(`# CLAUDE.md — AI Context for ${stack.name}`);
1967
+ lines.push('');
1968
+ lines.push('> This file is automatically loaded by Claude Code every session.');
1969
+ lines.push('> Keep it under 300 tokens. For deeper context, see /ai-docs/.');
1970
+ lines.push('');
1971
+ // Project
1972
+ lines.push('## Project');
1973
+ lines.push(`**Name:** ${stack.name}`);
1974
+ lines.push(`**Description:** ${answers.description}`);
1975
+ if (stack.framework)
1976
+ lines.push(`**Framework:** ${stack.framework}`);
1977
+ if (stack.language)
1978
+ lines.push(`**Language:** ${stack.language}`);
1979
+ if (stack.database)
1980
+ lines.push(`**Database:** ${stack.database}`);
1981
+ if (stack.auth)
1982
+ lines.push(`**Auth:** ${stack.auth}`);
1983
+ if (stack.payments)
1984
+ lines.push(`**Payments:** ${stack.payments}`);
1985
+ if (stack.styling)
1986
+ lines.push(`**Styling:** ${stack.styling}`);
1987
+ if (stack.testing)
1988
+ lines.push(`**Testing:** ${stack.testing}`);
1989
+ if (stack.extras.length > 0)
1990
+ lines.push(`**Extras:** ${stack.extras.join(', ')}`);
1991
+ lines.push('');
1992
+ // Commands
1993
+ if (Object.keys(stack.commands).length > 0) {
1994
+ lines.push('## Key Commands');
1995
+ if (stack.commands.dev)
1996
+ lines.push(`- **Dev:** \`${stack.commands.dev}\``);
1997
+ if (stack.commands.build)
1998
+ lines.push(`- **Build:** \`${stack.commands.build}\``);
1999
+ if (stack.commands.test)
2000
+ lines.push(`- **Test:** \`${stack.commands.test}\``);
2001
+ if (stack.commands.lint)
2002
+ lines.push(`- **Lint:** \`${stack.commands.lint}\``);
2003
+ if (stack.commands.dbPush)
2004
+ lines.push(`- **DB push:** \`${stack.commands.dbPush}\``);
2005
+ if (stack.commands.dbMigrate)
2006
+ lines.push(`- **DB migrate:** \`${stack.commands.dbMigrate}\``);
2007
+ if (stack.commands.dbStudio)
2008
+ lines.push(`- **DB studio:** \`${stack.commands.dbStudio}\``);
2009
+ lines.push('');
2010
+ }
2011
+ // Conventions
2012
+ if (answers.conventions && answers.conventions.length > 0) {
2013
+ lines.push('## Coding Conventions (Always Follow)');
2014
+ answers.conventions.forEach(c => lines.push(`- ${c}`));
2015
+ lines.push('');
2016
+ }
2017
+ // Never do
2018
+ if (answers.never && answers.never.length > 0) {
2019
+ lines.push('## Never Do');
2020
+ answers.never.forEach(n => lines.push(`- ❌ ${n}`));
2021
+ lines.push('');
2022
+ }
2023
+ // Git
2024
+ if (answers.gitFlow !== undefined) {
2025
+ lines.push('## Git Workflow');
2026
+ if (answers.gitFlow) {
2027
+ lines.push('- Feature branches for all changes');
2028
+ lines.push('- PRs required before merging to main');
2029
+ lines.push('- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`');
2030
+ }
2031
+ else {
2032
+ lines.push('- Direct commits to main branch');
2033
+ lines.push('- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`');
2034
+ }
2035
+ lines.push('');
2036
+ }
2037
+ // Extra
2038
+ if (answers.extra) {
2039
+ lines.push('## Additional Context');
2040
+ lines.push(answers.extra);
2041
+ lines.push('');
2042
+ }
2043
+ // Domain docs pointer
2044
+ lines.push('## AI Docs (Load When Relevant)');
2045
+ lines.push('Deeper context files in `/ai-docs/` — load these when working on that domain:');
2046
+ lines.push('');
2047
+ const domainFiles = getDomainFiles(stack, answers);
2048
+ domainFiles.forEach(d => lines.push(`- \`/ai-docs/${d.filename}\` — ${d.label}`));
2049
+ lines.push('');
2050
+ lines.push('---');
2051
+ lines.push('*Generated by [claude-context](https://github.com/your-username/claude-context)*');
2052
+ return lines.join('\n');
2053
+ }
2054
+ function databaseDoc(stack) {
2055
+ const db = stack.database || 'your database';
2056
+ return `# Database Context
2057
+
2058
+ ## Stack
2059
+ **ORM / Client:** ${db}
2060
+ ${stack.hasMigrations ? '**Migrations:** Yes — always run migrations before deploying' : ''}
2061
+
2062
+ ## Patterns
2063
+
2064
+ ### Querying
2065
+ ${db.includes('Prisma') ? `- Use \`prisma\` client from \`@/lib/prisma\`
2066
+ - Always use \`prisma.$transaction\` for multi-step writes
2067
+ - Use \`select\` to limit returned fields — never return full objects to the client
2068
+ - Soft deletes with \`deletedAt\` field where applicable` : ''}
2069
+ ${db.includes('Supabase') ? `- Use the Supabase server client for server-side queries
2070
+ - Use the Supabase browser client only for real-time subscriptions
2071
+ - Row Level Security (RLS) is enabled — always check policies` : ''}
2072
+ ${db.includes('Drizzle') ? `- Use \`db\` from \`@/lib/db\`
2073
+ - Prefer \`db.select().from(table).where(...)\` over raw SQL
2074
+ - Use \`db.transaction()\` for multi-step writes` : ''}
2075
+ ${db.includes('Mongoose') ? `- Use models from \`@/models/\`
2076
+ - Always use \`.lean()\` for read-only queries
2077
+ - Use \`session\` for multi-document transactions` : ''}
2078
+
2079
+ ### Error Handling
2080
+ - Wrap all DB calls in try/catch
2081
+ - Log errors with context (userId, operation, tableName)
2082
+ - Return typed errors, never throw raw DB errors to the client
2083
+
2084
+ ### Performance
2085
+ - Add indexes for all foreign keys and frequently queried fields
2086
+ - Use pagination — never return unbounded lists
2087
+ - Avoid N+1 queries — use \`include\`/\`join\` or batch queries
2088
+
2089
+ ## Schema Conventions
2090
+ - All tables have: \`id\`, \`createdAt\`, \`updatedAt\`
2091
+ - Foreign key naming: \`userId\`, \`postId\` (camelCase)
2092
+ - Boolean fields: \`isActive\`, \`isPublished\` (not \`active\`, \`published\`)
2093
+ - Timestamps: \`createdAt\`, \`deletedAt\` — ISO 8601 strings
2094
+
2095
+ ## Files
2096
+ - Schema definition: \`prisma/schema.prisma\` or \`src/db/schema.ts\`
2097
+ - DB client: \`src/lib/db.ts\` or \`src/lib/prisma.ts\`
2098
+ - Seed data: \`prisma/seed.ts\` or \`scripts/seed.ts\`
2099
+ `;
2100
+ }
2101
+ function authDoc(stack) {
2102
+ const auth = stack.auth || 'custom auth';
2103
+ return `# Authentication Context
2104
+
2105
+ ## Stack
2106
+ **Provider:** ${auth}
2107
+
2108
+ ## How Auth Works in This App
2109
+ ${auth.includes('NextAuth') ? `- Session managed by NextAuth.js
2110
+ - \`getServerSession()\` for server-side session access
2111
+ - \`useSession()\` hook for client-side
2112
+ - Session includes: \`user.id\`, \`user.email\`, \`user.role\`
2113
+ - Protected routes use middleware in \`middleware.ts\`` : ''}
2114
+ ${auth.includes('Clerk') ? `- Auth managed by Clerk
2115
+ - \`auth()\` for server components, \`useAuth()\` for client
2116
+ - \`currentUser()\` to get full user object server-side
2117
+ - Middleware in \`middleware.ts\` protects all non-public routes
2118
+ - Webhooks at \`/api/webhooks/clerk\` sync users to DB` : ''}
2119
+ ${auth.includes('Supabase') ? `- Auth managed by Supabase
2120
+ - \`createServerClient()\` for server-side auth
2121
+ - \`createBrowserClient()\` for client-side auth
2122
+ - Always verify session server-side — never trust client claims` : ''}
2123
+ ${auth.includes('JWT') ? `- JWT tokens stored in httpOnly cookies
2124
+ - Verify token in middleware for protected routes
2125
+ - Access token expires in 15min, refresh token in 7 days
2126
+ - Never store JWTs in localStorage` : ''}
2127
+
2128
+ ## Rules
2129
+ - ❌ Never expose user passwords or tokens in responses
2130
+ - ❌ Never trust user-supplied IDs without verifying against session
2131
+ - ✅ Always check auth on the server, not just the client
2132
+ - ✅ Use middleware for route protection, not per-page checks
2133
+
2134
+ ## Roles & Permissions
2135
+ - Roles stored in \`user.role\` (e.g. \`admin\`, \`user\`, \`viewer\`)
2136
+ - Check permissions server-side before any sensitive operation
2137
+ - Use typed enums for roles — never hardcode string comparisons
2138
+
2139
+ ## Files
2140
+ - Auth config: \`src/lib/auth.ts\` or \`auth.config.ts\`
2141
+ - Middleware: \`middleware.ts\` (root level)
2142
+ - Auth API routes: \`app/api/auth/\` or \`pages/api/auth/\`
2143
+ `;
2144
+ }
2145
+ function paymentsDoc(stack) {
2146
+ const pay = stack.payments || 'Stripe';
2147
+ return `# Payments Context
2148
+
2149
+ ## Stack
2150
+ **Provider:** ${pay}
2151
+
2152
+ ## Architecture
2153
+ ${pay.includes('Stripe') ? `- Stripe handles all payment processing — we never store card data
2154
+ - Webhooks at \`/api/webhooks/stripe\` update subscription state
2155
+ - Customer ID stored in DB: \`users.stripeCustomerId\`
2156
+ - Subscription status: \`active\`, \`trialing\`, \`canceled\`, \`past_due\`
2157
+
2158
+ ## Key Flows
2159
+ **Checkout:** User → \`/api/checkout\` → Stripe Checkout Session → redirect → webhook confirms
2160
+ **Portal:** User → \`/api/billing/portal\` → Stripe Customer Portal → webhook on change
2161
+ **Webhook:** Stripe → \`/api/webhooks/stripe\` → verify signature → update DB` : ''}
2162
+ ${pay.includes('Lemon') ? `- Lemon Squeezy handles all payment processing
2163
+ - Webhooks at \`/api/webhooks/lemonsqueezy\`
2164
+ - Subscription status synced to DB on every webhook event` : ''}
2165
+
2166
+ ## Rules
2167
+ - ❌ Never process payments client-side
2168
+ - ❌ Never trust subscription status from the client — always check DB
2169
+ - ✅ Always verify webhook signatures before processing
2170
+ - ✅ Idempotency: check if webhook event was already processed
2171
+
2172
+ ## Subscription Gates
2173
+ - Check \`user.subscriptionStatus === 'active'\` before gating features
2174
+ - Handle \`trialing\`, \`past_due\` states gracefully
2175
+ - Free tier limits enforced server-side
2176
+
2177
+ ## Files
2178
+ - Stripe client: \`src/lib/stripe.ts\`
2179
+ - Webhook handler: \`app/api/webhooks/stripe/route.ts\`
2180
+ - Checkout handler: \`app/api/checkout/route.ts\`
2181
+ `;
2182
+ }
2183
+ function apiDoc(stack) {
2184
+ return `# API Design Context
2185
+
2186
+ ## Pattern
2187
+ ${stack.framework === 'Next.js' ? `- Route Handlers in \`app/api/\` for REST endpoints
2188
+ - Server Actions in \`app/actions/\` for form mutations
2189
+ - Use Server Actions over API routes for form submissions` : '- RESTful endpoints, grouped by resource'}
2190
+
2191
+ ## Request / Response Shape
2192
+ \`\`\`typescript
2193
+ // Success
2194
+ { data: T, error: null }
2195
+
2196
+ // Error
2197
+ { data: null, error: { message: string, code: string } }
2198
+ \`\`\`
2199
+
2200
+ ## Rules
2201
+ - ✅ Always validate request bodies with Zod (or equivalent)
2202
+ - ✅ Return consistent error shapes
2203
+ - ✅ Include HTTP status codes that match the outcome
2204
+ - ❌ Never return raw DB errors to the client
2205
+ - ❌ Never expose internal IDs in public-facing APIs
2206
+
2207
+ ## Auth on API Routes
2208
+ - Check auth at the top of every protected handler
2209
+ - Return 401 for unauthenticated, 403 for unauthorized
2210
+ \`\`\`typescript
2211
+ const session = await getServerSession();
2212
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
2213
+ \`\`\`
2214
+
2215
+ ## Validation
2216
+ - Use Zod schemas defined in \`src/lib/validations/\`
2217
+ - Validate both incoming and outgoing data
2218
+ - Strip unknown fields before passing to DB
2219
+
2220
+ ## Rate Limiting
2221
+ - Apply rate limiting to all public endpoints
2222
+ - Use IP-based limiting for unauthenticated routes
2223
+ `;
2224
+ }
2225
+ function frontendDoc(stack) {
2226
+ return `# Frontend Patterns Context
2227
+
2228
+ ## Component Architecture
2229
+ ${stack.framework === 'Next.js' ? `- Default to Server Components — use \`'use client'\` only when needed
2230
+ - Client components needed for: hooks, event listeners, browser APIs, animations
2231
+ - Keep client components small — push state down, data up` : '- Functional components only, no class components'}
2232
+
2233
+ ## File Conventions
2234
+ \`\`\`
2235
+ components/
2236
+ ui/ # Generic, reusable (Button, Input, Modal)
2237
+ [feature]/ # Feature-specific (UserProfile, PaymentForm)
2238
+ app/ or pages/ # Route components only — minimal logic here
2239
+ hooks/ # Custom React hooks
2240
+ lib/ # Utilities, helpers, configs
2241
+ types/ # TypeScript interfaces and types
2242
+ \`\`\`
2243
+
2244
+ ## Styling
2245
+ ${stack.styling === 'Tailwind CSS' ? `- Tailwind CSS for all styling — no inline styles, no CSS modules
2246
+ - Use \`cn()\` utility (clsx + tailwind-merge) for conditional classes
2247
+ - Responsive: mobile-first, \`sm:\`, \`md:\`, \`lg:\` breakpoints
2248
+ - Dark mode: use \`dark:\` variants, not manual class toggling` : `- Follow the existing styling pattern in the codebase`}
2249
+
2250
+ ## State Management
2251
+ ${stack.extras.includes('Zustand') ? '- Global state in Zustand stores — \`src/stores/\`' : ''}
2252
+ ${stack.extras.includes('React Query') ? '- Server state via React Query — never duplicate in local state' : ''}
2253
+ - Local UI state: useState/useReducer
2254
+ - URL state: searchParams for filters/pagination
2255
+ - Avoid prop drilling beyond 2 levels — use context or stores
2256
+
2257
+ ## Rules
2258
+ - ❌ Never fetch data directly in client components — use Server Components or React Query
2259
+ - ❌ Never hardcode colors or spacing — use Tailwind tokens
2260
+ - ✅ All interactive elements must have accessible labels
2261
+ - ✅ Loading and error states required for all async operations
2262
+ `;
2263
+ }
2264
+ function errorHandlingDoc(stack) {
2265
+ return `# Error Handling Context
2266
+
2267
+ ## Philosophy
2268
+ - Errors are expected — handle them gracefully, log them faithfully
2269
+ - User-facing errors: friendly messages, no stack traces
2270
+ - Server logs: full context (userId, operation, input shape, stack trace)
2271
+
2272
+ ## Layers
2273
+
2274
+ ### Server (API Routes / Server Actions)
2275
+ \`\`\`typescript
2276
+ try {
2277
+ const result = await someOperation();
2278
+ return { data: result, error: null };
2279
+ } catch (error) {
2280
+ console.error('[OPERATION_NAME]', { error, userId, input });
2281
+ return { data: null, error: { message: 'Something went wrong', code: 'INTERNAL_ERROR' } };
2282
+ }
2283
+ \`\`\`
2284
+
2285
+ ### Client (React Components)
2286
+ \`\`\`typescript
2287
+ // Async operations
2288
+ const [error, setError] = useState<string | null>(null);
2289
+ // Handle errors in catch, display with error toast or inline message
2290
+
2291
+ // Form submissions
2292
+ if (!result.success) {
2293
+ toast.error(result.error.message);
2294
+ return;
2295
+ }
2296
+ \`\`\`
2297
+
2298
+ ## Error Codes
2299
+ Define typed error codes in \`src/lib/errors.ts\`:
2300
+ - \`UNAUTHORIZED\` — user not logged in
2301
+ - \`FORBIDDEN\` — logged in but not allowed
2302
+ - \`NOT_FOUND\` — resource doesn't exist
2303
+ - \`VALIDATION_ERROR\` — invalid input
2304
+ - \`INTERNAL_ERROR\` — unexpected server error
2305
+
2306
+ ## What NOT to Do
2307
+ - ❌ Empty catch blocks
2308
+ - ❌ \`console.log\` instead of \`console.error\` for errors
2309
+ - ❌ Returning raw error.message from database errors to the client
2310
+ - ❌ Using generic "Something went wrong" for validation errors — be specific
2311
+ `;
2312
+ }
2313
+ function testingDoc(stack) {
2314
+ const testLib = stack.testing || 'Vitest';
2315
+ return `# Testing Context
2316
+
2317
+ ## Stack
2318
+ **Framework:** ${testLib}
2319
+
2320
+ ## What to Test
2321
+ - ✅ Business logic / utility functions — unit tests
2322
+ - ✅ API route handlers — integration tests with mocked DB
2323
+ - ✅ Critical user flows — E2E tests (checkout, auth, core feature)
2324
+ - ❌ Don't test implementation details — test behavior
2325
+
2326
+ ## File Conventions
2327
+ \`\`\`
2328
+ src/
2329
+ lib/
2330
+ utils.ts
2331
+ utils.test.ts # Co-located unit tests
2332
+ components/
2333
+ Button.tsx
2334
+ Button.test.tsx # Component tests
2335
+ tests/
2336
+ e2e/ # End-to-end tests
2337
+ \`\`\`
2338
+
2339
+ ## Test Patterns
2340
+ \`\`\`typescript
2341
+ // Unit test example
2342
+ describe('formatPrice', () => {
2343
+ it('formats cents to dollars', () => {
2344
+ expect(formatPrice(1999)).toBe('$19.99');
2345
+ });
2346
+ it('handles zero', () => {
2347
+ expect(formatPrice(0)).toBe('$0.00');
2348
+ });
2349
+ });
2350
+ \`\`\`
2351
+
2352
+ ## Mocking
2353
+ - Mock external services (Stripe, email, DB) in tests
2354
+ - Use \`vi.mock()\` (Vitest) or \`jest.mock()\` for module mocks
2355
+ - Don't mock internal utilities — test them directly
2356
+
2357
+ ## CI
2358
+ - Tests run on every PR
2359
+ - No merging with failing tests
2360
+ - Coverage threshold: 70% for critical paths
2361
+ `;
2362
+ }
2363
+ // ─────────────────────────────────────────────────────────────────────────────
2364
+ // Deployment doc — Railway-specific
2365
+ // ─────────────────────────────────────────────────────────────────────────────
2366
+ function deploymentDoc(stack) {
2367
+ return `# Deployment Context
2368
+
2369
+ ## Platform: Railway
2370
+
2371
+ ### Overview
2372
+ - All services deployed to Railway (web app + PostgreSQL + Redis if needed)
2373
+ - Deployments trigger automatically on push to \`main\`
2374
+ - Each PR can get an ephemeral preview environment if configured
2375
+ - Environment variables set in Railway dashboard — never hardcode
2376
+
2377
+ ### Services Layout
2378
+ \`\`\`
2379
+ Railway Project
2380
+ ├── web ← Next.js app (or API)
2381
+ ├── postgres ← Railway-managed PostgreSQL
2382
+ └── worker ← Background jobs (if needed)
2383
+ \`\`\`
2384
+
2385
+ ### Environment Variables
2386
+ Always required in Railway dashboard:
2387
+ \`\`\`
2388
+ DATABASE_URL # Provided by Railway PostgreSQL plugin
2389
+ NODE_ENV # production
2390
+ NEXTAUTH_URL # https://your-domain.com (or Railway URL)
2391
+ \`\`\`
2392
+
2393
+ Access in code:
2394
+ \`\`\`typescript
2395
+ // Always use process.env — never import.meta.env in Node context
2396
+ const db = process.env.DATABASE_URL;
2397
+ \`\`\`
2398
+
2399
+ ### Database: PostgreSQL on Railway
2400
+ - Connection string format: \`postgresql://user:pass@host:port/dbname\`
2401
+ - Railway provides \`DATABASE_URL\` automatically when PostgreSQL is added
2402
+ - Use connection pooling for production (PgBouncer or Prisma's \`?pgbouncer=true\`)
2403
+ - SSL required: append \`?sslmode=require\` if not using Prisma
2404
+
2405
+ \`\`\`typescript
2406
+ // Prisma — railway-safe config in schema.prisma
2407
+ datasource db {
2408
+ provider = "postgresql"
2409
+ url = env("DATABASE_URL")
2410
+ directUrl = env("DIRECT_URL") // for migrations only
2411
+ }
2412
+ \`\`\`
2413
+
2414
+ \`\`\`typescript
2415
+ // Drizzle — railway-safe config
2416
+ import { drizzle } from 'drizzle-orm/node-postgres';
2417
+ import { Pool } from 'pg';
2418
+
2419
+ const pool = new Pool({
2420
+ connectionString: process.env.DATABASE_URL,
2421
+ ssl: { rejectUnauthorized: false }, // required on Railway
2422
+ });
2423
+ export const db = drizzle(pool);
2424
+ \`\`\`
2425
+
2426
+ ### Migrations on Railway
2427
+ - Run migrations as part of the deploy command, not a separate step
2428
+ - Set Railway start command to: \`npm run db:migrate && npm run start\`
2429
+ - Or use \`railway run prisma migrate deploy\` in CI
2430
+
2431
+ ### Cron Jobs on Railway
2432
+ Railway supports cron jobs as separate services:
2433
+
2434
+ \`\`\`
2435
+ # In Railway: add a Cron service
2436
+ # Schedule format: standard cron (minute hour day month weekday)
2437
+ 0 9 * * 1-5 # 9am weekdays
2438
+ 0 0 * * * # midnight daily
2439
+ */15 * * * * # every 15 minutes
2440
+ \`\`\`
2441
+
2442
+ Cron job patterns:
2443
+ \`\`\`typescript
2444
+ // app/api/cron/[job]/route.ts
2445
+ export async function GET(req: Request) {
2446
+ // Verify it's coming from Railway / internal
2447
+ const secret = req.headers.get('x-cron-secret');
2448
+ if (secret !== process.env.CRON_SECRET) {
2449
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
2450
+ }
2451
+
2452
+ // Always make cron handlers idempotent
2453
+ // Check if job already ran for this period before doing work
2454
+ try {
2455
+ await runJob();
2456
+ return Response.json({ success: true });
2457
+ } catch (error) {
2458
+ console.error('[CRON ERROR]', error);
2459
+ return Response.json({ error: 'Job failed' }, { status: 500 });
2460
+ }
2461
+ }
2462
+ \`\`\`
2463
+
2464
+ ### Cloudflare in Front of Railway
2465
+ - Railway app sits behind Cloudflare (DNS proxy enabled)
2466
+ - Cloudflare handles: SSL termination, DDoS protection, caching static assets
2467
+ - Set Railway custom domain → Cloudflare DNS → Railway
2468
+
2469
+ Cache rules (set in Cloudflare dashboard):
2470
+ - Cache static assets (\`/_next/static/*\`): Cache Everything, Edge TTL 1 month
2471
+ - API routes (\`/api/*\`): Bypass Cache
2472
+ - Pages: Browser cache only, no edge cache (unless explicitly static)
2473
+
2474
+ Headers to set in Railway app:
2475
+ \`\`\`typescript
2476
+ // next.config.js
2477
+ headers: [
2478
+ {
2479
+ source: '/_next/static/:path*',
2480
+ headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
2481
+ },
2482
+ ]
2483
+ \`\`\`
2484
+
2485
+ ### Health Check
2486
+ Railway pings \`/api/health\` — always implement it:
2487
+ \`\`\`typescript
2488
+ // app/api/health/route.ts
2489
+ export async function GET() {
2490
+ return Response.json({ status: 'ok', ts: Date.now() });
2491
+ }
2492
+ \`\`\`
2493
+
2494
+ ### Deploy Checklist
2495
+ - [ ] All env vars set in Railway dashboard
2496
+ - [ ] \`DATABASE_URL\` connected to Railway PostgreSQL service
2497
+ - [ ] Migrations run as part of start command
2498
+ - [ ] Health check endpoint live at \`/api/health\`
2499
+ - [ ] Custom domain added + Cloudflare DNS configured
2500
+ - [ ] Stripe webhook URL updated to production domain
2501
+ - [ ] \`CRON_SECRET\` set if using cron jobs
2502
+ `;
2503
+ }
2504
+ // ─────────────────────────────────────────────────────────────────────────────
2505
+ // Email doc — Resend-specific
2506
+ // ─────────────────────────────────────────────────────────────────────────────
2507
+ function emailDoc(stack) {
2508
+ return `# Email Context
2509
+
2510
+ ## Provider: Resend
2511
+
2512
+ ### Setup
2513
+ \`\`\`typescript
2514
+ // src/lib/email.ts
2515
+ import { Resend } from 'resend';
2516
+
2517
+ export const resend = new Resend(process.env.RESEND_API_KEY);
2518
+ \`\`\`
2519
+
2520
+ Required env vars:
2521
+ \`\`\`
2522
+ RESEND_API_KEY=re_xxxxxxxxxxxx
2523
+ EMAIL_FROM=noreply@yourdomain.com # must be verified in Resend
2524
+ \`\`\`
2525
+
2526
+ ### Sending Emails
2527
+ \`\`\`typescript
2528
+ // Always send from a server action or API route — never client-side
2529
+ import { resend } from '@/lib/email';
2530
+
2531
+ const { data, error } = await resend.emails.send({
2532
+ from: process.env.EMAIL_FROM!,
2533
+ to: user.email,
2534
+ subject: 'Welcome to the app',
2535
+ react: <WelcomeEmail name={user.name} />, // or html: '...'
2536
+ });
2537
+
2538
+ if (error) {
2539
+ console.error('[EMAIL_SEND_ERROR]', error);
2540
+ // Don't throw — email failure shouldn't block the user flow
2541
+ // Log and continue, or queue for retry
2542
+ }
2543
+ \`\`\`
2544
+
2545
+ ### Email Templates
2546
+ Store React email components in \`src/emails/\`:
2547
+ \`\`\`
2548
+ src/
2549
+ emails/
2550
+ welcome.tsx # Welcome / onboarding
2551
+ magic-link.tsx # Auth magic link
2552
+ payment-receipt.tsx # Post-purchase
2553
+ subscription.tsx # Subscription changes
2554
+ password-reset.tsx # Password reset
2555
+ \`\`\`
2556
+
2557
+ Template pattern:
2558
+ \`\`\`typescript
2559
+ // src/emails/welcome.tsx
2560
+ import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
2561
+
2562
+ interface WelcomeEmailProps {
2563
+ name: string;
2564
+ loginUrl: string;
2565
+ }
2566
+
2567
+ export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
2568
+ return (
2569
+ <Html>
2570
+ <Head />
2571
+ <Body>
2572
+ <Container>
2573
+ <Text>Hi {name},</Text>
2574
+ <Button href={loginUrl}>Get started</Button>
2575
+ </Container>
2576
+ </Body>
2577
+ </Html>
2578
+ );
2579
+ }
2580
+ \`\`\`
2581
+
2582
+ ### Rules
2583
+ - ❌ Never send emails from client components
2584
+ - ❌ Never hardcode \`from\` addresses — always use env var
2585
+ - ❌ Never block user flows on email failure — log and continue
2586
+ - ✅ Always verify the \`from\` domain in Resend dashboard
2587
+ - ✅ Log email send failures with context (userId, type, error)
2588
+ - ✅ Use React Email for HTML emails — never raw HTML strings
2589
+ - ✅ Test with Resend's test mode before going live
2590
+
2591
+ ### Transactional Email Triggers
2592
+ | Event | Template | Timing |
2593
+ |-------|----------|--------|
2594
+ | User signs up | welcome.tsx | Immediate |
2595
+ | Magic link / OTP | magic-link.tsx | Immediate |
2596
+ | Payment successful | payment-receipt.tsx | On Stripe webhook |
2597
+ | Subscription canceled | subscription.tsx | On Stripe webhook |
2598
+ | Password reset | password-reset.tsx | On request |
2599
+
2600
+ ### DNS Setup (Cloudflare)
2601
+ Add Resend's DNS records in Cloudflare:
2602
+ - SPF TXT record on root domain
2603
+ - DKIM CNAME records (Resend provides these)
2604
+ - Set proxy status to **DNS only** (grey cloud) for mail records — not proxied
2605
+ `;
2606
+ }
2607
+ // ─────────────────────────────────────────────────────────────────────────────
2608
+ // Analytics doc — GA4 + Google Search Console
2609
+ // ─────────────────────────────────────────────────────────────────────────────
2610
+ function analyticsDoc(stack) {
2611
+ return `# Analytics Context
2612
+
2613
+ ## Stack
2614
+ - **Google Analytics 4 (GA4)** — user behaviour, conversions, funnels
2615
+ - **Google Search Console** — SEO performance, indexing, search queries
2616
+
2617
+ ---
2618
+
2619
+ ## Google Analytics 4
2620
+
2621
+ ### Setup
2622
+ \`\`\`
2623
+ GA_MEASUREMENT_ID=G-XXXXXXXXXX # in .env
2624
+ \`\`\`
2625
+
2626
+ \`\`\`typescript
2627
+ // src/lib/analytics.ts
2628
+ export const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!;
2629
+
2630
+ // Page view
2631
+ export function pageview(url: string) {
2632
+ if (typeof window === 'undefined' || !GA_ID) return;
2633
+ window.gtag('config', GA_ID, { page_path: url });
2634
+ }
2635
+
2636
+ // Custom event
2637
+ export function trackEvent(
2638
+ action: string,
2639
+ category: string,
2640
+ label?: string,
2641
+ value?: number
2642
+ ) {
2643
+ if (typeof window === 'undefined' || !GA_ID) return;
2644
+ window.gtag('event', action, {
2645
+ event_category: category,
2646
+ event_label: label,
2647
+ value,
2648
+ });
2649
+ }
2650
+ \`\`\`
2651
+
2652
+ ### Next.js Integration
2653
+ \`\`\`typescript
2654
+ // app/layout.tsx — add GA script
2655
+ import Script from 'next/script';
2656
+
2657
+ <Script
2658
+ src={\`https://www.googletagmanager.com/gtag/js?id=\${GA_ID}\`}
2659
+ strategy="afterInteractive"
2660
+ />
2661
+ <Script id="ga-init" strategy="afterInteractive">
2662
+ {\`
2663
+ window.dataLayer = window.dataLayer || [];
2664
+ function gtag(){dataLayer.push(arguments);}
2665
+ gtag('js', new Date());
2666
+ gtag('config', '\${GA_ID}', { page_path: window.location.pathname });
2667
+ \`}
2668
+ </Script>
2669
+ \`\`\`
2670
+
2671
+ ### Key Events to Track
2672
+ \`\`\`typescript
2673
+ // Conversion events — track these
2674
+ trackEvent('sign_up', 'auth', method); // User registers
2675
+ trackEvent('login', 'auth', method); // User logs in
2676
+ trackEvent('begin_checkout', 'ecommerce', plan); // Hits checkout
2677
+ trackEvent('purchase', 'ecommerce', plan, amount); // Payment complete
2678
+ trackEvent('cancel_subscription', 'billing'); // Cancels plan
2679
+
2680
+ // Engagement events
2681
+ trackEvent('feature_used', 'engagement', featureName);
2682
+ trackEvent('share', 'engagement', contentType);
2683
+ \`\`\`
2684
+
2685
+ ### Cloudflare + GA4
2686
+ - Cloudflare can block GA4 if Rocket Loader is enabled — disable it or defer GA scripts
2687
+ - Use \`strategy="afterInteractive"\` in Next.js to avoid blocking render
2688
+
2689
+ ### Rules
2690
+ - ❌ Never track PII (emails, names) in GA4 events
2691
+ - ❌ Never fire events server-side — GA4 is client-only (use GA4 Measurement Protocol for server events)
2692
+ - ✅ Always check \`typeof window !== 'undefined'\` before calling gtag
2693
+ - ✅ Respect user consent — don't load GA until cookie consent given (if required by region)
2694
+
2695
+ ---
2696
+
2697
+ ## Google Search Console
2698
+
2699
+ ### Setup
2700
+ 1. Add property in Search Console: \`https://yourdomain.com\`
2701
+ 2. Verify via Cloudflare DNS TXT record (easiest method)
2702
+ 3. Submit sitemap: \`https://yourdomain.com/sitemap.xml\`
2703
+
2704
+ ### Sitemap in Next.js
2705
+ \`\`\`typescript
2706
+ // app/sitemap.ts
2707
+ import { MetadataRoute } from 'next';
2708
+
2709
+ export default function sitemap(): MetadataRoute.Sitemap {
2710
+ return [
2711
+ { url: 'https://yourdomain.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
2712
+ { url: 'https://yourdomain.com/pricing', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
2713
+ // Add dynamic pages here
2714
+ ];
2715
+ }
2716
+ \`\`\`
2717
+
2718
+ ### Robots.txt
2719
+ \`\`\`typescript
2720
+ // app/robots.ts
2721
+ export default function robots() {
2722
+ return {
2723
+ rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/dashboard/'] },
2724
+ sitemap: 'https://yourdomain.com/sitemap.xml',
2725
+ };
2726
+ }
2727
+ \`\`\`
2728
+
2729
+ ### What to Monitor Weekly
2730
+ - **Coverage** — indexing errors, excluded pages
2731
+ - **Performance** — clicks, impressions, CTR, average position
2732
+ - **Core Web Vitals** — LCP, FID, CLS scores
2733
+ - **Search queries** — what terms bring people in
2734
+ `;
2735
+ }
2736
+ // ─────────────────────────────────────────────────────────────────────────────
2737
+ // Cloudflare doc
2738
+ // ─────────────────────────────────────────────────────────────────────────────
2739
+ function cloudflareDoc(stack) {
2740
+ return `# Cloudflare Context
2741
+
2742
+ ## Role in This Stack
2743
+ Cloudflare sits in front of Railway:
2744
+ \`\`\`
2745
+ User → Cloudflare (DNS + CDN + DDoS) → Railway (app) → PostgreSQL
2746
+ \`\`\`
2747
+
2748
+ ## DNS Setup
2749
+ - Domain managed in Cloudflare DNS
2750
+ - Railway custom domain → CNAME pointing to Railway-provided URL
2751
+ - Proxy status: **Proxied** (orange cloud) for the main app
2752
+ - Proxy status: **DNS only** (grey cloud) for: mail records (Resend), Railway health checks
2753
+
2754
+ ## SSL/TLS
2755
+ - Mode: **Full (strict)** — Cloudflare ↔ Railway both use SSL
2756
+ - Always Use HTTPS: **On**
2757
+ - Min TLS Version: **TLS 1.2**
2758
+ - Railway: enable "Generate Certificate" in Railway domain settings
2759
+
2760
+ ## Caching Rules
2761
+ Set in Cloudflare **Cache Rules** (not Page Rules — those are legacy):
2762
+
2763
+ | Pattern | Cache Behaviour |
2764
+ |---------|----------------|
2765
+ | \`/_next/static/*\` | Cache Everything, Edge TTL: 1 month |
2766
+ | \`/images/*\` | Cache Everything, Edge TTL: 1 week |
2767
+ | \`/api/*\` | Bypass Cache |
2768
+ | \`/dashboard*\` | Bypass Cache |
2769
+ | Everything else | Standard (Cloudflare decides) |
2770
+
2771
+ ## Security Rules
2772
+ Firewall → Custom Rules:
2773
+
2774
+ \`\`\`
2775
+ # Block suspicious bots (example rule in Cloudflare expression syntax)
2776
+ (cf.client.bot) and not (cf.verified_bot_category in {"Search Engine Crawlers"})
2777
+ → Action: Challenge
2778
+
2779
+ # Rate limit login attempts
2780
+ (http.request.uri.path eq "/api/auth/signin" and http.request.method eq "POST")
2781
+ → Rate limit: 10 req/min per IP → Action: Block
2782
+ \`\`\`
2783
+
2784
+ ## Workers (if needed)
2785
+ Use Cloudflare Workers for:
2786
+ - Edge redirects (faster than Next.js middleware for simple redirects)
2787
+ - A/B testing at the edge
2788
+ - Geo-based routing
2789
+
2790
+ Avoid Workers for:
2791
+ - Anything needing DB access (use Railway API instead)
2792
+ - Complex business logic (keep in Next.js)
2793
+
2794
+ ## Headers to Set in Next.js (Railway)
2795
+ These complement Cloudflare's protections:
2796
+ \`\`\`typescript
2797
+ // next.config.js
2798
+ const securityHeaders = [
2799
+ { key: 'X-Frame-Options', value: 'DENY' },
2800
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
2801
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
2802
+ { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
2803
+ ];
2804
+ \`\`\`
2805
+
2806
+ ## Purge Cache After Deploy
2807
+ After Railway deploys:
2808
+ \`\`\`bash
2809
+ # Purge Cloudflare cache via API
2810
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \\
2811
+ -H "Authorization: Bearer CF_API_TOKEN" \\
2812
+ -H "Content-Type: application/json" \\
2813
+ --data '{"purge_everything":true}'
2814
+ \`\`\`
2815
+ Or add this to Railway deploy hooks.
2816
+
2817
+ ## Env Vars Needed
2818
+ \`\`\`
2819
+ CLOUDFLARE_ZONE_ID=xxxx # For cache purge scripts
2820
+ CLOUDFLARE_API_TOKEN=xxxx # Scoped to Cache Purge permission only
2821
+ \`\`\`
2822
+
2823
+ ## Rules
2824
+ - ❌ Never put real-time API responses behind Cloudflare cache
2825
+ - ❌ Never proxy mail DNS records through Cloudflare (breaks Resend DKIM)
2826
+ - ✅ Always use Full (strict) SSL mode — never Flexible
2827
+ - ✅ Purge cache after significant deploys
2828
+ - ✅ Use Cloudflare Analytics as a secondary source (privacy-friendly, no cookies)
2829
+ `;
2830
+ }
2831
+ // ---------------------------------------------------------------------------
2832
+ // Constants & Lookup Functions
2833
+ // ---------------------------------------------------------------------------
2834
+ const ALL_DOMAIN_DOCS = [
2835
+ { filename: 'database.md', label: 'DB patterns, queries, schema conventions', key: 'database' },
2836
+ { filename: 'auth.md', label: 'Auth patterns, session access, route protection', key: 'auth' },
2837
+ { filename: 'payments.md', label: 'Payment flows, webhooks, subscription gates', key: 'payments' },
2838
+ { filename: 'api.md', label: 'API design, validation, error shapes', key: 'api' },
2839
+ { filename: 'frontend.md', label: 'Component patterns, styling, state management', key: 'frontend' },
2840
+ { filename: 'errors.md', label: 'Error handling patterns, logging, error codes', key: 'errors' },
2841
+ { filename: 'testing.md', label: 'Testing strategy, patterns, what to test', key: 'testing' },
2842
+ { filename: 'deployment.md', label: 'Railway deploy, crons, PostgreSQL, Cloudflare', key: 'deployment' },
2843
+ { filename: 'email.md', label: 'Resend setup, templates, triggers', key: 'email' },
2844
+ { filename: 'analytics.md', label: 'GA4 events, Search Console, sitemap', key: 'analytics' },
2845
+ { filename: 'cloudflare.md', label: 'DNS, caching rules, security, SSL', key: 'cloudflare' },
2846
+ ];
2847
+ exports.ALL_DOMAIN_DOCS = ALL_DOMAIN_DOCS;
2848
+ const SKILL_FILES = [
2849
+ { filename: 'skills/frontend.md', label: 'Frontend Engineer persona' },
2850
+ { filename: 'skills/backend.md', label: 'Backend Engineer persona' },
2851
+ { filename: 'skills/devops.md', label: 'DevOps / Infrastructure persona' },
2852
+ { filename: 'skills/reviewer.md', label: 'Code Reviewer persona' },
2853
+ ];
2854
+ exports.SKILL_FILES = SKILL_FILES;
2855
+ const EXTRA_FILES = [
2856
+ { filename: 'security.md', label: 'OWASP + STRIDE threat model for this stack' },
2857
+ { filename: 'HANDOVER.md', label: 'Session handover template (resume sessions cheaply)' },
2858
+ { filename: 'MISTAKES.md', label: 'Living mistake log (never repeat the same error)' },
2859
+ { filename: 'TOKEN-RULES.md', label: 'Token-saving rules for Claude' },
2860
+ ];
2861
+ exports.EXTRA_FILES = EXTRA_FILES;
2862
+ function getDomainFiles(stack, answers) {
2863
+ const selected = (answers.domains || []).map((d) => d.toLowerCase());
2864
+ const alwaysDocs = answers.alwaysDocs || [];
2865
+ const files = [];
2866
+ for (const doc of ALL_DOMAIN_DOCS) {
2867
+ const shouldInclude = alwaysDocs.includes(doc.key) ||
2868
+ selected.some((d) => d.includes(doc.key)) ||
2869
+ (doc.key === 'database' && !!stack.database) ||
2870
+ (doc.key === 'auth' && !!stack.auth) ||
2871
+ (doc.key === 'payments' && !!stack.payments);
2872
+ if (shouldInclude)
2873
+ files.push(doc);
2874
+ }
2875
+ if (files.length === 0) {
2876
+ return ALL_DOMAIN_DOCS.filter(d => ['api', 'frontend', 'deployment'].includes(d.key));
2877
+ }
2878
+ return files;
2879
+ }
2880
+ function generateDomainDoc(filename, stack) {
2881
+ switch (filename) {
2882
+ case 'database.md': return databaseDoc(stack);
2883
+ case 'auth.md': return authDoc(stack);
2884
+ case 'payments.md': return paymentsDoc(stack);
2885
+ case 'api.md': return apiDoc(stack);
2886
+ case 'frontend.md': return frontendDoc(stack);
2887
+ case 'errors.md': return errorHandlingDoc(stack);
2888
+ case 'testing.md': return testingDoc(stack);
2889
+ case 'deployment.md': return deploymentDoc(stack);
2890
+ case 'email.md': return emailDoc(stack);
2891
+ case 'analytics.md': return analyticsDoc(stack);
2892
+ case 'cloudflare.md': return cloudflareDoc(stack);
2893
+ default: return `# ${filename}\n\nAdd your context here.\n`;
2894
+ }
2895
+ }
2896
+ function generatePromptFile(filename, stack) {
2897
+ switch (filename) {
2898
+ case 'PROMPTS.md': return promptsIndex(stack);
2899
+ case 'prompts/auth.md': return authPrompts(stack);
2900
+ case 'prompts/database.md': return databasePrompts(stack);
2901
+ case 'prompts/payments.md': return paymentsPrompts(stack);
2902
+ case 'prompts/ui.md': return uiPrompts(stack);
2903
+ case 'prompts/api.md': return apiPrompts(stack);
2904
+ case 'prompts/deployment.md': return deploymentPrompts(stack);
2905
+ case 'prompts/email.md': return emailPrompts(stack);
2906
+ case 'prompts/analytics.md': return analyticsPrompts(stack);
2907
+ case 'prompts/security.md': return securityPrompts(stack);
2908
+ default: return `# ${filename}\n\nAdd prompt patterns here.\n`;
2909
+ }
2910
+ }
2911
+ function getPromptFiles(stack, answers) {
2912
+ const files = [{ filename: 'PROMPTS.md', label: 'Pattern library index' }];
2913
+ const domains = answers.domains || [];
2914
+ if (stack.auth || domains.includes('auth'))
2915
+ files.push({ filename: 'prompts/auth.md', label: 'Auth Build/Verify/Debug patterns' });
2916
+ if (stack.database || domains.includes('database'))
2917
+ files.push({ filename: 'prompts/database.md', label: 'Database Build/Verify/Debug patterns' });
2918
+ if (stack.payments || domains.includes('payments'))
2919
+ files.push({ filename: 'prompts/payments.md', label: 'Payments Build/Verify/Debug patterns' });
2920
+ files.push({ filename: 'prompts/ui.md', label: 'UI / component patterns' });
2921
+ files.push({ filename: 'prompts/api.md', label: 'API route patterns' });
2922
+ if (domains.includes('deployment') || answers.alwaysDocs?.includes('deployment'))
2923
+ files.push({ filename: 'prompts/deployment.md', label: 'Deployment / cron patterns' });
2924
+ if (domains.includes('email') || answers.alwaysDocs?.includes('email'))
2925
+ files.push({ filename: 'prompts/email.md', label: 'Email send patterns' });
2926
+ if (domains.includes('analytics') || answers.alwaysDocs?.includes('analytics'))
2927
+ files.push({ filename: 'prompts/analytics.md', label: 'Analytics event patterns' });
2928
+ files.push({ filename: 'prompts/security.md', label: 'Security / OWASP patterns' });
2929
+ return files;
2930
+ }
2931
+ function generateSkillFile(filename) {
2932
+ switch (filename) {
2933
+ case 'skills/frontend.md': return frontendSkill();
2934
+ case 'skills/backend.md': return backendSkill();
2935
+ case 'skills/devops.md': return devopsSkill();
2936
+ case 'skills/reviewer.md': return reviewerSkill();
2937
+ default: return '';
2938
+ }
2939
+ }
2940
+ function generateExtraFile(filename, stack) {
2941
+ switch (filename) {
2942
+ case 'security.md': return securityDoc(stack);
2943
+ case 'HANDOVER.md': return handoverTemplate();
2944
+ case 'MISTAKES.md': return mistakesTemplate();
2945
+ case 'TOKEN-RULES.md': return tokenRulesDoc();
2946
+ default: return '';
2947
+ }
2948
+ }
2949
+ // ---------------------------------------------------------------------------
2950
+ // File Writing Helpers
2951
+ // ---------------------------------------------------------------------------
2952
+ function writeFileSync(filePath, content) {
2953
+ const dir = path.dirname(filePath);
2954
+ if (!fs.existsSync(dir))
2955
+ fs.mkdirSync(dir, { recursive: true });
2956
+ fs.writeFileSync(filePath, content, 'utf8');
2957
+ }
2958
+ function checkExisting(cwd) {
2959
+ const existing = [];
2960
+ if (fs.existsSync(path.join(cwd, 'CLAUDE.md')))
2961
+ existing.push('CLAUDE.md');
2962
+ if (fs.existsSync(path.join(cwd, 'ai-docs')))
2963
+ existing.push('ai-docs/');
2964
+ return existing;
2965
+ }
2966
+ // ---------------------------------------------------------------------------
2967
+ // Main Generation Function
2968
+ // ---------------------------------------------------------------------------
2969
+ function generateContextFiles(projectDir, stack, options, answers = {}) {
2970
+ const { force = false, dryRun = false } = options;
2971
+ (0, logger_1.log)('GENERATOR', 'start', 'Generating context files', { projectDir, dryRun });
2972
+ // Check for existing files
2973
+ const existing = checkExisting(projectDir);
2974
+ if (existing.length > 0 && !force) {
2975
+ (0, logger_1.logWarn)('GENERATOR', 'existing', 'Existing files found, use --force to overwrite', { existing });
2976
+ return;
2977
+ }
2978
+ // Determine which files to generate
2979
+ const domainFiles = getDomainFiles(stack, answers);
2980
+ const promptFiles = getPromptFiles(stack, answers);
2981
+ if (dryRun) {
2982
+ (0, logger_1.log)('GENERATOR', 'dry-run', 'Dry run - would generate:');
2983
+ console.log('Layer 1: CLAUDE.md');
2984
+ domainFiles.forEach((f) => console.log('Layer 2: ai-docs/' + f.filename));
2985
+ promptFiles.forEach((f) => console.log('Layer 3: ai-docs/' + f.filename));
2986
+ SKILL_FILES.forEach((f) => console.log('Skills: ai-docs/' + f.filename));
2987
+ EXTRA_FILES.forEach((f) => console.log('Extras: ai-docs/' + f.filename));
2988
+ return;
2989
+ }
2990
+ // Layer 1 - CLAUDE.md
2991
+ (0, logger_1.log)('GENERATOR', 'write', 'Writing Layer 1 - CLAUDE.md');
2992
+ const claudeMdContent = claudeMd(stack, answers);
2993
+ writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMdContent);
2994
+ (0, logger_1.log)('GENERATOR', 'write', 'CLAUDE.md', { tokens: Math.round(claudeMdContent.length / 4) });
2995
+ // Layer 2 - Domain Docs
2996
+ (0, logger_1.log)('GENERATOR', 'write', 'Writing Layer 2 - Domain Docs');
2997
+ for (const f of domainFiles) {
2998
+ const content = generateDomainDoc(f.filename, stack);
2999
+ writeFileSync(path.join(projectDir, 'ai-docs', f.filename), content);
3000
+ (0, logger_1.log)('GENERATOR', 'write', f.filename, { label: f.label, tokens: Math.round(content.length / 4) });
3001
+ }
3002
+ // Layer 3 - Pattern Library
3003
+ (0, logger_1.log)('GENERATOR', 'write', 'Writing Layer 3 - Pattern Library');
3004
+ for (const f of promptFiles) {
3005
+ const content = generatePromptFile(f.filename, stack);
3006
+ writeFileSync(path.join(projectDir, 'ai-docs', f.filename), content);
3007
+ (0, logger_1.log)('GENERATOR', 'write', f.filename, { label: f.label, tokens: Math.round(content.length / 4) });
3008
+ }
3009
+ // Skills
3010
+ (0, logger_1.log)('GENERATOR', 'write', 'Writing Skills');
3011
+ for (const f of SKILL_FILES) {
3012
+ const content = generateSkillFile(f.filename);
3013
+ writeFileSync(path.join(projectDir, 'ai-docs', f.filename), content);
3014
+ (0, logger_1.log)('GENERATOR', 'write', f.filename, { label: f.label });
3015
+ }
3016
+ // Extras
3017
+ (0, logger_1.log)('GENERATOR', 'write', 'Writing Extras');
3018
+ for (const f of EXTRA_FILES) {
3019
+ const content = generateExtraFile(f.filename, stack);
3020
+ writeFileSync(path.join(projectDir, 'ai-docs', f.filename), content);
3021
+ (0, logger_1.log)('GENERATOR', 'write', f.filename, { label: f.label });
3022
+ }
3023
+ (0, logger_1.log)('GENERATOR', 'done', 'All context files generated successfully');
3024
+ }
3025
+ //# sourceMappingURL=generator.js.map