@levironexe/architect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +55 -0
  3. package/README.md +341 -0
  4. package/dist/analyzers/ast-parser.d.ts +3 -0
  5. package/dist/analyzers/ast-parser.js +305 -0
  6. package/dist/analyzers/ast-parser.js.map +1 -0
  7. package/dist/analyzers/dependency-graph.d.ts +2 -0
  8. package/dist/analyzers/dependency-graph.js +67 -0
  9. package/dist/analyzers/dependency-graph.js.map +1 -0
  10. package/dist/analyzers/duplication.d.ts +2 -0
  11. package/dist/analyzers/duplication.js +56 -0
  12. package/dist/analyzers/duplication.js.map +1 -0
  13. package/dist/analyzers/file-walker.d.ts +3 -0
  14. package/dist/analyzers/file-walker.js +80 -0
  15. package/dist/analyzers/file-walker.js.map +1 -0
  16. package/dist/cli/context-runner.d.ts +1 -0
  17. package/dist/cli/context-runner.js +16 -0
  18. package/dist/cli/context-runner.js.map +1 -0
  19. package/dist/cli/index.d.ts +24 -0
  20. package/dist/cli/index.js +217 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/init-runner.d.ts +25 -0
  23. package/dist/cli/init-runner.js +152 -0
  24. package/dist/cli/init-runner.js.map +1 -0
  25. package/dist/cli/scan-runner.d.ts +8 -0
  26. package/dist/cli/scan-runner.js +133 -0
  27. package/dist/cli/scan-runner.js.map +1 -0
  28. package/dist/formatters/plan-json.d.ts +2 -0
  29. package/dist/formatters/plan-json.js +4 -0
  30. package/dist/formatters/plan-json.js.map +1 -0
  31. package/dist/formatters/plan-markdown.d.ts +2 -0
  32. package/dist/formatters/plan-markdown.js +42 -0
  33. package/dist/formatters/plan-markdown.js.map +1 -0
  34. package/dist/formatters/plan-prompt.d.ts +4 -0
  35. package/dist/formatters/plan-prompt.js +5 -0
  36. package/dist/formatters/plan-prompt.js.map +1 -0
  37. package/dist/formatters/plan-terminal.d.ts +5 -0
  38. package/dist/formatters/plan-terminal.js +62 -0
  39. package/dist/formatters/plan-terminal.js.map +1 -0
  40. package/dist/generators/blueprint-renderer.d.ts +3 -0
  41. package/dist/generators/blueprint-renderer.js +27 -0
  42. package/dist/generators/blueprint-renderer.js.map +1 -0
  43. package/dist/generators/claudeWriter.d.ts +3 -0
  44. package/dist/generators/claudeWriter.js +9 -0
  45. package/dist/generators/claudeWriter.js.map +1 -0
  46. package/dist/generators/copilotWriter.d.ts +3 -0
  47. package/dist/generators/copilotWriter.js +11 -0
  48. package/dist/generators/copilotWriter.js.map +1 -0
  49. package/dist/generators/cursorWriter.d.ts +3 -0
  50. package/dist/generators/cursorWriter.js +14 -0
  51. package/dist/generators/cursorWriter.js.map +1 -0
  52. package/dist/generators/genericWriter.d.ts +3 -0
  53. package/dist/generators/genericWriter.js +9 -0
  54. package/dist/generators/genericWriter.js.map +1 -0
  55. package/dist/generators/template-context.d.ts +18 -0
  56. package/dist/generators/template-context.js +126 -0
  57. package/dist/generators/template-context.js.map +1 -0
  58. package/dist/generators/templateRenderer.d.ts +2 -0
  59. package/dist/generators/templateRenderer.js +19 -0
  60. package/dist/generators/templateRenderer.js.map +1 -0
  61. package/dist/generators/windsurfWriter.d.ts +3 -0
  62. package/dist/generators/windsurfWriter.js +14 -0
  63. package/dist/generators/windsurfWriter.js.map +1 -0
  64. package/dist/generators/writer-types.d.ts +11 -0
  65. package/dist/generators/writer-types.js +40 -0
  66. package/dist/generators/writer-types.js.map +1 -0
  67. package/dist/llm/claude-provider.d.ts +8 -0
  68. package/dist/llm/claude-provider.js +22 -0
  69. package/dist/llm/claude-provider.js.map +1 -0
  70. package/dist/llm/concern-classifier.d.ts +15 -0
  71. package/dist/llm/concern-classifier.js +61 -0
  72. package/dist/llm/concern-classifier.js.map +1 -0
  73. package/dist/llm/config.d.ts +11 -0
  74. package/dist/llm/config.js +120 -0
  75. package/dist/llm/config.js.map +1 -0
  76. package/dist/llm/ollama-provider.d.ts +8 -0
  77. package/dist/llm/ollama-provider.js +27 -0
  78. package/dist/llm/ollama-provider.js.map +1 -0
  79. package/dist/llm/openai-provider.d.ts +8 -0
  80. package/dist/llm/openai-provider.js +19 -0
  81. package/dist/llm/openai-provider.js.map +1 -0
  82. package/dist/llm/prompt-builder.d.ts +12 -0
  83. package/dist/llm/prompt-builder.js +132 -0
  84. package/dist/llm/prompt-builder.js.map +1 -0
  85. package/dist/llm/provider.d.ts +17 -0
  86. package/dist/llm/provider.js +2 -0
  87. package/dist/llm/provider.js.map +1 -0
  88. package/dist/llm/response-parser.d.ts +6 -0
  89. package/dist/llm/response-parser.js +128 -0
  90. package/dist/llm/response-parser.js.map +1 -0
  91. package/dist/planner/plan-generator.d.ts +7 -0
  92. package/dist/planner/plan-generator.js +275 -0
  93. package/dist/planner/plan-generator.js.map +1 -0
  94. package/dist/planner/plan-prompt-builder.d.ts +9 -0
  95. package/dist/planner/plan-prompt-builder.js +92 -0
  96. package/dist/planner/plan-prompt-builder.js.map +1 -0
  97. package/dist/planner/plan-response-parser.d.ts +7 -0
  98. package/dist/planner/plan-response-parser.js +21 -0
  99. package/dist/planner/plan-response-parser.js.map +1 -0
  100. package/dist/planner/plan-validator.d.ts +3 -0
  101. package/dist/planner/plan-validator.js +49 -0
  102. package/dist/planner/plan-validator.js.map +1 -0
  103. package/dist/reporters/scan-json.d.ts +13 -0
  104. package/dist/reporters/scan-json.js +26 -0
  105. package/dist/reporters/scan-json.js.map +1 -0
  106. package/dist/reporters/terminal.d.ts +6 -0
  107. package/dist/reporters/terminal.js +224 -0
  108. package/dist/reporters/terminal.js.map +1 -0
  109. package/dist/scoring/consistency-score.d.ts +3 -0
  110. package/dist/scoring/consistency-score.js +23 -0
  111. package/dist/scoring/consistency-score.js.map +1 -0
  112. package/dist/scoring/duplication-score.d.ts +3 -0
  113. package/dist/scoring/duplication-score.js +16 -0
  114. package/dist/scoring/duplication-score.js.map +1 -0
  115. package/dist/scoring/health-score.d.ts +4 -0
  116. package/dist/scoring/health-score.js +20 -0
  117. package/dist/scoring/health-score.js.map +1 -0
  118. package/dist/scoring/issue-builder.d.ts +4 -0
  119. package/dist/scoring/issue-builder.js +62 -0
  120. package/dist/scoring/issue-builder.js.map +1 -0
  121. package/dist/scoring/modularity-score.d.ts +3 -0
  122. package/dist/scoring/modularity-score.js +56 -0
  123. package/dist/scoring/modularity-score.js.map +1 -0
  124. package/dist/scoring/pattern-analysis.d.ts +3 -0
  125. package/dist/scoring/pattern-analysis.js +74 -0
  126. package/dist/scoring/pattern-analysis.js.map +1 -0
  127. package/dist/scoring/separation-score.d.ts +3 -0
  128. package/dist/scoring/separation-score.js +35 -0
  129. package/dist/scoring/separation-score.js.map +1 -0
  130. package/dist/skills/detector.d.ts +4 -0
  131. package/dist/skills/detector.js +104 -0
  132. package/dist/skills/detector.js.map +1 -0
  133. package/dist/skills/lister.d.ts +9 -0
  134. package/dist/skills/lister.js +35 -0
  135. package/dist/skills/lister.js.map +1 -0
  136. package/dist/skills/loader.d.ts +6 -0
  137. package/dist/skills/loader.js +76 -0
  138. package/dist/skills/loader.js.map +1 -0
  139. package/dist/skills/structure-check.d.ts +2 -0
  140. package/dist/skills/structure-check.js +37 -0
  141. package/dist/skills/structure-check.js.map +1 -0
  142. package/dist/skills/validator.d.ts +6 -0
  143. package/dist/skills/validator.js +229 -0
  144. package/dist/skills/validator.js.map +1 -0
  145. package/dist/types/analysis.d.ts +130 -0
  146. package/dist/types/analysis.js +41 -0
  147. package/dist/types/analysis.js.map +1 -0
  148. package/dist/types/concern.d.ts +48 -0
  149. package/dist/types/concern.js +16 -0
  150. package/dist/types/concern.js.map +1 -0
  151. package/dist/types/generation.d.ts +32 -0
  152. package/dist/types/generation.js +2 -0
  153. package/dist/types/generation.js.map +1 -0
  154. package/dist/types/issue.d.ts +12 -0
  155. package/dist/types/issue.js +2 -0
  156. package/dist/types/issue.js.map +1 -0
  157. package/dist/types/pattern.d.ts +15 -0
  158. package/dist/types/pattern.js +2 -0
  159. package/dist/types/pattern.js.map +1 -0
  160. package/dist/types/plan.d.ts +56 -0
  161. package/dist/types/plan.js +2 -0
  162. package/dist/types/plan.js.map +1 -0
  163. package/dist/types/scan-output.d.ts +84 -0
  164. package/dist/types/scan-output.js +2 -0
  165. package/dist/types/scan-output.js.map +1 -0
  166. package/dist/types/scoring.d.ts +15 -0
  167. package/dist/types/scoring.js +2 -0
  168. package/dist/types/scoring.js.map +1 -0
  169. package/dist/types/skill.d.ts +97 -0
  170. package/dist/types/skill.js +2 -0
  171. package/dist/types/skill.js.map +1 -0
  172. package/dist/utils/agent-detector.d.ts +2 -0
  173. package/dist/utils/agent-detector.js +22 -0
  174. package/dist/utils/agent-detector.js.map +1 -0
  175. package/dist/utils/interactive.d.ts +6 -0
  176. package/dist/utils/interactive.js +15 -0
  177. package/dist/utils/interactive.js.map +1 -0
  178. package/dist/utils/path.d.ts +5 -0
  179. package/dist/utils/path.js +31 -0
  180. package/dist/utils/path.js.map +1 -0
  181. package/dist/utils/progress.d.ts +17 -0
  182. package/dist/utils/progress.js +48 -0
  183. package/dist/utils/progress.js.map +1 -0
  184. package/dist/utils/thresholds.d.ts +6 -0
  185. package/dist/utils/thresholds.js +48 -0
  186. package/dist/utils/thresholds.js.map +1 -0
  187. package/package.json +63 -0
  188. package/skills/meta/general-js.skill.yaml +131 -0
  189. package/skills/patterns/clerk-auth.skill.yaml +349 -0
  190. package/skills/patterns/docker-deploy.skill.yaml +214 -0
  191. package/skills/patterns/drizzle.skill.yaml +277 -0
  192. package/skills/patterns/mongoose.skill.yaml +290 -0
  193. package/skills/patterns/nextauth.skill.yaml +308 -0
  194. package/skills/patterns/playwright-e2e.skill.yaml +265 -0
  195. package/skills/patterns/prisma.skill.yaml +255 -0
  196. package/skills/patterns/s3-storage.skill.yaml +235 -0
  197. package/skills/patterns/selenium-e2e.skill.yaml +276 -0
  198. package/skills/patterns/supabase-auth.skill.yaml +298 -0
  199. package/skills/patterns/supabase.skill.yaml +304 -0
  200. package/skills/patterns/vercel-deploy.skill.yaml +219 -0
  201. package/skills/patterns/vitest-testing.skill.yaml +262 -0
  202. package/skills/stacks/express-api.skill.yaml +155 -0
  203. package/skills/stacks/fastify-api.skill.yaml +119 -0
  204. package/skills/stacks/hono-api.skill.yaml +130 -0
  205. package/skills/stacks/nestjs.skill.yaml +135 -0
  206. package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
  207. package/skills/stacks/react-spa.skill.yaml +153 -0
  208. package/skills/stacks/vue-nuxt.skill.yaml +115 -0
  209. package/templates/architect-plan.md +139 -0
  210. package/templates/architect-refactor.md +119 -0
@@ -0,0 +1,349 @@
1
+ schema_version: "2.0.0"
2
+ id: clerk-auth
3
+ name: "Clerk Auth"
4
+ version: "2.0.0"
5
+ description: "Managed authentication with Clerk — middleware route protection, server/client user access patterns, webhook database sync, and organization-based multi-tenancy."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - clerk
10
+ dependencies:
11
+ none:
12
+ - next-auth
13
+ - "@supabase/ssr"
14
+ - lucia
15
+ detection:
16
+ dependencies:
17
+ any:
18
+ - "@clerk/nextjs"
19
+ - "@clerk/clerk-sdk-node"
20
+ - "@clerk/clerk-react"
21
+ source_indicators:
22
+ - "ClerkProvider"
23
+ - "clerkMiddleware"
24
+ - "auth()"
25
+ - "currentUser()"
26
+ - "useUser()"
27
+ - "useAuth()"
28
+ structure:
29
+ required_dirs:
30
+ - path: app/api/webhooks/clerk
31
+ purpose: "Clerk webhook handler route — receives user.created, user.updated, and user.deleted events pushed by Clerk's servers. Must verify the Svix signature on every incoming request before touching the database — no signature check means attackers can forge any user lifecycle event."
32
+ recommended_dirs:
33
+ - path: src/lib
34
+ purpose: "Server-only Clerk helpers — auth() call wrappers, organization guard functions, and backend Clerk SDK calls (e.g., clerkClient().users.updateUserMetadata). Never import from here in Client Components ('use client' files)."
35
+ - path: middleware.ts
36
+ purpose: "Project root middleware file — clerkMiddleware() runs before every page render and API call, enforcing public vs. protected route separation. Only one middleware.ts is allowed per Next.js project; it must be at the root alongside app/."
37
+ separation:
38
+ rules:
39
+ - concern: route_protection
40
+ belongs_in: middleware.ts
41
+ rule_text: "Use clerkMiddleware() in middleware.ts to protect routes. Define public routes with createRouteMatcher — every other route is protected by default and redirects to sign-in. Never add per-page auth checks in layout.tsx or page.tsx as a substitute for middleware."
42
+ example: |
43
+ // middleware.ts — runs before every request
44
+ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
45
+
46
+ const isPublicRoute = createRouteMatcher([
47
+ '/sign-in(.*)',
48
+ '/sign-up(.*)',
49
+ '/api/webhooks/(.*)',
50
+ '/',
51
+ ]);
52
+
53
+ export default clerkMiddleware(async (auth, req) => {
54
+ if (!isPublicRoute(req)) await auth.protect();
55
+ });
56
+
57
+ export const config = {
58
+ matcher: [
59
+ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
60
+ '/(api|trpc)(.*)',
61
+ ],
62
+ };
63
+ indicators:
64
+ - "clerkMiddleware"
65
+ - "createRouteMatcher"
66
+ - "auth.protect()"
67
+ - "isPublicRoute"
68
+ - concern: server_user_access
69
+ belongs_in: app
70
+ rule_text: "In Server Components and API route handlers, use auth() from @clerk/nextjs/server to get the current session (userId, orgId). For full user profile data (name, email, imageUrl), use currentUser() — it makes a network call to Clerk's API so only call it when the profile data is actually needed."
71
+ example: |
72
+ // app/dashboard/page.tsx — Server Component
73
+ import { auth, currentUser } from '@clerk/nextjs/server';
74
+ import { redirect } from 'next/navigation';
75
+
76
+ export default async function DashboardPage() {
77
+ const { userId } = await auth();
78
+ if (!userId) redirect('/sign-in');
79
+
80
+ // Only call currentUser() when you need profile fields beyond userId
81
+ const user = await currentUser();
82
+ return <h1>Welcome, {user?.firstName}</h1>;
83
+ }
84
+
85
+ // app/api/user/route.ts — API route
86
+ import { auth } from '@clerk/nextjs/server';
87
+ export async function GET() {
88
+ const { userId } = await auth();
89
+ if (!userId) return new Response('Unauthorized', { status: 401 });
90
+ const userData = await db.user.findUnique({ where: { clerkId: userId } });
91
+ return Response.json(userData);
92
+ }
93
+ indicators:
94
+ - "from '@clerk/nextjs/server'"
95
+ - "auth()"
96
+ - "currentUser()"
97
+ - "userId"
98
+ - concern: client_user_access
99
+ belongs_in: components
100
+ rule_text: "In Client Components, use useUser(), useAuth(), or useClerk() hooks from @clerk/nextjs. These hooks read from the ClerkProvider context — they work only inside components rendered below <ClerkProvider> in the tree. Use for display-only UI (avatars, names, role badges) — never for access control decisions."
101
+ example: |
102
+ // components/user-avatar.tsx — Client Component
103
+ 'use client';
104
+ import { useUser } from '@clerk/nextjs';
105
+
106
+ export function UserAvatar() {
107
+ const { user, isLoaded, isSignedIn } = useUser();
108
+
109
+ // isLoaded prevents hydration mismatch
110
+ if (!isLoaded) return <div className="h-8 w-8 rounded-full bg-gray-200 animate-pulse" />;
111
+ if (!isSignedIn) return null;
112
+
113
+ return (
114
+ <img
115
+ src={user.imageUrl}
116
+ alt={user.fullName ?? 'User avatar'}
117
+ className="h-8 w-8 rounded-full"
118
+ />
119
+ );
120
+ }
121
+ indicators:
122
+ - "useUser()"
123
+ - "useAuth()"
124
+ - "useClerk()"
125
+ - "from '@clerk/nextjs'"
126
+ - concern: webhook_sync
127
+ belongs_in: app/api/webhooks/clerk
128
+ rule_text: "Verify the Svix signature before processing any webhook event. Sync user lifecycle events (user.created, user.updated, user.deleted) to your database. Store Clerk's userId as a NOT NULL UNIQUE 'clerkId' column in your users table — never store email alone because users can change their email in Clerk's dashboard."
129
+ example: |
130
+ // app/api/webhooks/clerk/route.ts
131
+ import { Webhook } from 'svix';
132
+ import { headers } from 'next/headers';
133
+ import type { WebhookEvent } from '@clerk/nextjs/server';
134
+
135
+ export async function POST(req: Request) {
136
+ const body = await req.text();
137
+ const h = await headers();
138
+
139
+ const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
140
+ // wh.verify() throws WebhookVerificationError on invalid signature
141
+ const event = wh.verify(body, {
142
+ 'svix-id': h.get('svix-id')!,
143
+ 'svix-timestamp': h.get('svix-timestamp')!,
144
+ 'svix-signature': h.get('svix-signature')!,
145
+ }) as WebhookEvent;
146
+
147
+ switch (event.type) {
148
+ case 'user.created':
149
+ await db.user.create({
150
+ data: {
151
+ clerkId: event.data.id,
152
+ email: event.data.email_addresses[0].email_address,
153
+ name: `${event.data.first_name} ${event.data.last_name}`.trim(),
154
+ },
155
+ });
156
+ break;
157
+ case 'user.updated':
158
+ await db.user.update({
159
+ where: { clerkId: event.data.id },
160
+ data: { email: event.data.email_addresses[0].email_address },
161
+ });
162
+ break;
163
+ case 'user.deleted':
164
+ await db.user.delete({ where: { clerkId: event.data.id! } });
165
+ break;
166
+ }
167
+
168
+ return new Response('OK', { status: 200 });
169
+ }
170
+ indicators:
171
+ - "Webhook"
172
+ - "svix"
173
+ - "CLERK_WEBHOOK_SECRET"
174
+ - "WebhookEvent"
175
+ - "svix-signature"
176
+ - concern: organization_access
177
+ belongs_in: app
178
+ rule_text: "For multi-tenant apps, read orgId from auth() and filter every database query by it. Switching organizations in Clerk's UI rotates the orgId in the session — your database queries automatically see only the new org's data. Never derive the organization from user metadata or a custom header."
179
+ example: |
180
+ // app/projects/page.tsx — scoped to active organization
181
+ import { auth } from '@clerk/nextjs/server';
182
+ import { redirect } from 'next/navigation';
183
+
184
+ export default async function ProjectsPage() {
185
+ const { userId, orgId } = await auth();
186
+ if (!userId) redirect('/sign-in');
187
+ if (!orgId) redirect('/select-org'); // user hasn't selected an org yet
188
+
189
+ // Every query filtered by orgId — no cross-tenant data leaks
190
+ const projects = await db.project.findMany({
191
+ where: { orgId },
192
+ orderBy: { createdAt: 'desc' },
193
+ });
194
+
195
+ return <ProjectList projects={projects} />;
196
+ }
197
+ indicators:
198
+ - "orgId"
199
+ - "orgRole"
200
+ - "useOrganization()"
201
+ - "useOrganizationList()"
202
+ - "orgSlug"
203
+ patterns:
204
+ data_flow:
205
+ direction: "HTTP Request → clerkMiddleware (protect or allow) → Server Component (auth()) → Service/DB"
206
+ rules:
207
+ - "Middleware runs first — protected routes redirect to sign-in before any page code executes."
208
+ - "Server Components call auth() for session (userId, orgId) and currentUser() for profile data — currentUser() costs an extra network call so only call it when needed."
209
+ - "Client Components use useUser()/useAuth() hooks — they read from ClerkProvider context already in the page, zero extra network call."
210
+ - "Webhooks are the bridge from Clerk's managed user store to your relational database — sync on user.created/updated/deleted events."
211
+ - "For multi-tenant apps: auth() → { userId, orgId } → every DB query includes orgId in WHERE clause."
212
+ - "publicMetadata and privateMetadata must only be mutated from Server Actions or API routes, never from Client Components."
213
+ error_handling:
214
+ recommended: "Use auth.protect() in middleware for route-level protection. For API routes not covered by middleware, check auth().userId and return 401 when null. For org-scoped routes, check auth().orgId and redirect to /select-org when null."
215
+ naming:
216
+ middleware: "middleware.ts at project root — clerkMiddleware() must be the outermost and only middleware"
217
+ webhook_handler: "app/api/webhooks/clerk/route.ts — POST handler with Svix signature verification"
218
+ server_helper: "src/lib/auth.ts — reusable server-side auth wrappers (requireAuth, requireOrg)"
219
+ db_field: "clerkId — NOT NULL UNIQUE column in users table; stores Clerk's userId string (format: user_XXXX)"
220
+ org_db_field: "orgId — NOT NULL column on organization-scoped tables; stores Clerk's orgId string (format: org_XXXX)"
221
+ anti_patterns:
222
+ - id: manual_token_handling
223
+ severity: critical
224
+ description: "Manually reading, parsing, or verifying Clerk JWTs or __session cookies instead of using Clerk's auth() helper. This breaks when Clerk rotates signing keys (which happens automatically), and bypasses session freshness checks, leaving stale or forged tokens accepted."
225
+ bad_example: |
226
+ // ❌ Manual JWT parsing — breaks on key rotation, bypasses Clerk's checks
227
+ import jwt from 'jsonwebtoken';
228
+ const token = req.headers.authorization?.split(' ')[1];
229
+ const decoded = jwt.verify(token, process.env.CLERK_PEM_KEY!);
230
+ const userId = (decoded as any).sub;
231
+ // This will silently break the next time Clerk rotates its signing key
232
+ good_example: |
233
+ // ✓ Clerk's auth() handles verification, key rotation, and session refresh automatically
234
+ import { auth } from '@clerk/nextjs/server';
235
+ const { userId } = await auth();
236
+ if (!userId) return new Response('Unauthorized', { status: 401 });
237
+ - id: unverified_webhook
238
+ severity: critical
239
+ description: "Processing Clerk webhook payloads without verifying the Svix HMAC signature. Any attacker who discovers your webhook endpoint URL can POST fabricated user.created or user.deleted events — creating phantom users, deleting real users, or granting admin roles without ever touching Clerk's dashboard."
240
+ bad_example: |
241
+ // ❌ No signature verification — endpoint is open to forgery
242
+ export async function POST(req: Request) {
243
+ const payload = await req.json();
244
+ // Blindly trusting the payload type and data
245
+ if (payload.type === 'user.created') {
246
+ await db.user.create({
247
+ data: { clerkId: payload.data.id, role: 'admin' }, // forged!
248
+ });
249
+ }
250
+ return new Response('OK');
251
+ }
252
+ good_example: |
253
+ // ✓ Svix verification throws WebhookVerificationError on tampered requests
254
+ import { Webhook } from 'svix';
255
+ const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
256
+ const event = wh.verify(body, svixHeaders) as WebhookEvent;
257
+ // Only reaches here if signature is valid
258
+ if (event.type === 'user.created') {
259
+ await db.user.create({ data: { clerkId: event.data.id } });
260
+ }
261
+ - id: client_component_auth_check
262
+ severity: warning
263
+ description: "Using useUser() or useAuth() to gate protected content or navigation in Client Components. Client-side auth can be bypassed by disabling JavaScript, using browser DevTools to modify React state, or navigating directly via URL. Always enforce access control in middleware or Server Components."
264
+ bad_example: |
265
+ // ❌ Client-side guard — content flashes and is bypassable with DevTools
266
+ 'use client';
267
+ import { useAuth } from '@clerk/nextjs';
268
+ export function AdminPanel() {
269
+ const { isSignedIn } = useAuth();
270
+ if (!isSignedIn) return null; // briefly visible during hydration; bypassable
271
+ return <DangerousAdminActions />;
272
+ }
273
+ good_example: |
274
+ // ✓ Server Component — auth check happens before any HTML is sent
275
+ import { auth } from '@clerk/nextjs/server';
276
+ import { redirect } from 'next/navigation';
277
+ export default async function AdminPage() {
278
+ const { userId } = await auth();
279
+ if (!userId) redirect('/sign-in');
280
+ return <DangerousAdminActions />;
281
+ }
282
+ // OR: add /admin to isPublicRoute exclusion in middleware.ts (simplest)
283
+ - id: missing_clerk_provider
284
+ severity: critical
285
+ description: "Not wrapping the root layout with <ClerkProvider>. All Clerk client hooks (useUser, useAuth, useClerk, SignInButton, UserButton) throw a React context error at runtime because they expect to find ClerkProvider as an ancestor in the component tree."
286
+ bad_example: |
287
+ // app/layout.tsx — missing ClerkProvider
288
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
289
+ return (
290
+ <html lang="en">
291
+ <body>{children}</body>
292
+ {/* Any child that calls useUser() crashes:
293
+ Error: useUser() called outside <ClerkProvider> */}
294
+ </html>
295
+ );
296
+ }
297
+ good_example: |
298
+ // app/layout.tsx — ClerkProvider wraps the entire app
299
+ import { ClerkProvider } from '@clerk/nextjs';
300
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
301
+ return (
302
+ <ClerkProvider>
303
+ <html lang="en">
304
+ <body>{children}</body>
305
+ </html>
306
+ </ClerkProvider>
307
+ );
308
+ }
309
+ - id: org_id_not_enforced
310
+ severity: warning
311
+ description: "In multi-tenant apps, querying the database without filtering by orgId. Users authenticated to organization A can read or modify organization B's data because the tenant boundary is not applied to the query."
312
+ bad_example: |
313
+ // ❌ Returns ALL projects regardless of the user's current organization
314
+ const { userId } = await auth();
315
+ // orgId is available but ignored — org B members see org A's data
316
+ const projects = await db.project.findMany({ where: { createdBy: userId } });
317
+ good_example: |
318
+ // ✓ Scope every query to the authenticated user's active organization
319
+ const { userId, orgId } = await auth();
320
+ if (!orgId) redirect('/select-organization');
321
+ const projects = await db.project.findMany({
322
+ where: { orgId }, // org boundary enforced at query level
323
+ });
324
+ - id: metadata_mutation_on_client
325
+ severity: warning
326
+ description: "Updating Clerk publicMetadata from a Client Component via the browser SDK. The client-side user.update() call is made with the user's own session, meaning any user can call it with arbitrary values — including promoting themselves to 'admin'. All metadata writes must go through a Server Action that validates permissions first."
327
+ bad_example: |
328
+ // ❌ Client-side metadata write — user can call update() with any role value
329
+ 'use client';
330
+ import { useUser } from '@clerk/nextjs';
331
+ export function UpgradeButton() {
332
+ const { user } = useUser();
333
+ const upgrade = () => user?.update({ unsafeMetadata: { plan: 'enterprise' } });
334
+ return <button onClick={upgrade}>Upgrade</button>;
335
+ }
336
+ good_example: |
337
+ // ✓ Server Action validates the request before mutating metadata
338
+ 'use server';
339
+ import { auth, clerkClient } from '@clerk/nextjs/server';
340
+ export async function upgradePlan(targetUserId: string) {
341
+ const { sessionClaims } = await auth();
342
+ // Only admins can upgrade other users
343
+ if (sessionClaims?.publicMetadata?.role !== 'admin') {
344
+ throw new Error('Forbidden');
345
+ }
346
+ await (await clerkClient()).users.updateUserMetadata(targetUserId, {
347
+ publicMetadata: { plan: 'enterprise' },
348
+ });
349
+ }
@@ -0,0 +1,214 @@
1
+ schema_version: "2.0.0"
2
+ id: docker-deploy
3
+ name: "Docker"
4
+ version: "2.0.0"
5
+ description: "Docker containerization with multi-stage builds, non-root user, runtime-injected secrets, HEALTHCHECK, pinned base image tags, and correct COPY order to maximize layer cache."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks: []
9
+ dependencies:
10
+ none: []
11
+ detection:
12
+ files:
13
+ - Dockerfile
14
+ - docker-compose.yml
15
+ - docker-compose.yaml
16
+ source_indicators:
17
+ - "FROM node:"
18
+ - "COPY --from=builder"
19
+ - "docker-compose"
20
+ - "AS builder"
21
+ - "AS production"
22
+ structure:
23
+ required_dirs: []
24
+ recommended_dirs:
25
+ - path: docker
26
+ purpose: "Docker support files — entrypoint.sh (startup script that runs migrations before the app), healthcheck.sh (custom health probe script), and any docker-compose overrides. Keep these out of the project root to avoid clutter."
27
+ - path: .docker
28
+ purpose: "Alternative location for Docker configuration scripts — entrypoints, init scripts, and environment templates. Use one consistent location across the project."
29
+ separation:
30
+ rules:
31
+ - concern: multi_stage_build
32
+ belongs_in: Dockerfile
33
+ rule_text: "Use a two-stage Dockerfile — a `builder` stage installs all dependencies (including devDependencies) and compiles the TypeScript source, and a `production` stage copies only the compiled output. Never copy the entire source or node_modules from the builder if you can reinstall with --omit=dev."
34
+ example: |
35
+ # Dockerfile
36
+ # Stage 1: build
37
+ FROM node:20-alpine AS builder
38
+ WORKDIR /app
39
+ # Copy package files first — layer is cached until package.json changes
40
+ COPY package*.json ./
41
+ RUN npm ci
42
+ COPY . .
43
+ RUN npm run build # produces dist/
44
+
45
+ # Stage 2: production — only compiled output, no devDependencies
46
+ FROM node:20-alpine AS production
47
+ WORKDIR /app
48
+ COPY package*.json ./
49
+ RUN npm ci --omit=dev # install only runtime deps — ~70% smaller image
50
+ COPY --from=builder /app/dist ./dist
51
+ # Run health checks, switch user, start app
52
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
53
+ CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
54
+ USER node
55
+ CMD ["node", "dist/index.js"]
56
+ indicators:
57
+ - "FROM node:"
58
+ - "AS builder"
59
+ - "AS production"
60
+ - "COPY --from=builder"
61
+ - concern: non_root_user
62
+ belongs_in: Dockerfile
63
+ rule_text: "Switch to the built-in `node` user before the CMD or ENTRYPOINT instruction. Running as root in a container gives an attacker full host access if the container runtime is misconfigured or the app has a code execution vulnerability."
64
+ example: |
65
+ # ✓ Switch to non-root user — applied in the production stage
66
+ # Ensure the app directory is owned by node before switching
67
+ COPY --chown=node:node --from=builder /app/dist ./dist
68
+ USER node
69
+ CMD ["node", "dist/index.js"]
70
+ anti_indicators:
71
+ - "USER root"
72
+ - concern: env_injection
73
+ belongs_in: docker-compose.yml
74
+ rule_text: "Inject all secrets via environment variables at container runtime — never bake secrets into the image with ENV or ARG instructions. Use docker-compose's env_file directive to load from .env. Commit .env.example with placeholder values; add .env to .gitignore."
75
+ example: |
76
+ # docker-compose.yml — runtime environment injection
77
+ services:
78
+ app:
79
+ build: .
80
+ ports:
81
+ - "3000:3000"
82
+ env_file: .env # loads DATABASE_URL, JWT_SECRET, etc. from .env
83
+ environment:
84
+ NODE_ENV: production # non-secret config can be inline
85
+ restart: unless-stopped
86
+ indicators:
87
+ - "env_file:"
88
+ - "environment:"
89
+ - concern: layer_cache_order
90
+ belongs_in: Dockerfile
91
+ rule_text: "Order COPY instructions to maximize Docker's layer cache — copy package files (package.json, package-lock.json) and run npm ci BEFORE copying application source. Since package files change rarely, the npm ci layer is cached for the majority of builds."
92
+ example: |
93
+ # ✓ Package files first — npm ci layer cached unless package.json changes
94
+ COPY package*.json ./
95
+ RUN npm ci # cached on most builds
96
+ COPY . . # application source last — invalidates only subsequent layers
97
+ RUN npm run build
98
+
99
+ # ❌ WRONG ORDER — invalidates npm ci on every source file change:
100
+ # COPY . . # copies everything including src/
101
+ # RUN npm ci # re-runs on every code change — no cache benefit
102
+ # RUN npm run build
103
+ indicators:
104
+ - "COPY package*.json"
105
+ - concern: healthcheck
106
+ belongs_in: Dockerfile
107
+ rule_text: "Add a HEALTHCHECK instruction to every production Dockerfile. Without it, Docker and Kubernetes cannot distinguish between a starting container and a broken one — they keep routing traffic to unhealthy instances."
108
+ example: |
109
+ # Option 1: HTTP health check via node inline script (no extra dependencies)
110
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
111
+ CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
112
+
113
+ # Option 2: wget (available in alpine)
114
+ HEALTHCHECK --interval=30s --timeout=5s CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
115
+
116
+ # In docker-compose, override start-period for slow-starting apps:
117
+ # healthcheck:
118
+ # test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
119
+ # start_period: 30s
120
+ indicators:
121
+ - "HEALTHCHECK"
122
+ - "/health"
123
+ patterns:
124
+ data_flow:
125
+ direction: "Source → Builder Stage (npm ci + tsc) → Production Stage (npm ci --omit=dev + dist copy) → Container Runtime (env vars injected)"
126
+ rules:
127
+ - "Builder stage: install all deps + compile TypeScript to dist/."
128
+ - "Production stage: reinstall only runtime deps (--omit=dev) + copy dist/ from builder."
129
+ - "Secrets injected at runtime via env_file — never in Dockerfile ENV or ARG."
130
+ - "COPY package files before COPY . . to maximize layer cache reuse."
131
+ - "HEALTHCHECK enables orchestrators to detect unhealthy containers automatically."
132
+ error_handling:
133
+ recommended: "Add a /health endpoint that returns 200 when the app is ready (DB connected, etc.) and 503 during startup or degraded state. HEALTHCHECK uses this to report container health to Docker/Kubernetes."
134
+ naming:
135
+ dockerfile: "Dockerfile at project root — multi-stage: AS builder then AS production"
136
+ compose: "docker-compose.yml (development) + docker-compose.prod.yml (production overrides)"
137
+ env_template: ".env.example committed to git with placeholder values; .env in .gitignore"
138
+ health_endpoint: "GET /health — returns { status: 'ok', uptime: number } with 200, or 503 during degraded state"
139
+ anti_patterns:
140
+ - id: secrets_in_image
141
+ severity: critical
142
+ description: "Baking secrets into Docker images with ENV or ARG instructions. Secrets persist in image layers and are visible via `docker history` — anyone with pull access to the image registry can read them."
143
+ bad_example: |
144
+ # ❌ Secrets baked into the image — visible in docker history
145
+ ARG DATABASE_URL
146
+ ENV DATABASE_URL=${DATABASE_URL}
147
+ ENV JWT_SECRET=hardcoded-secret-key
148
+ # docker history myapp → shows JWT_SECRET value in the layer
149
+ good_example: |
150
+ # ✓ Inject secrets at runtime, not build time
151
+ # docker-compose.yml: env_file: .env
152
+ # Or: docker run --env-file .env myapp
153
+ # Image contains no secrets — safe to push to any registry
154
+ - id: single_stage_build
155
+ severity: warning
156
+ description: "Using a single-stage Dockerfile that includes all devDependencies, TypeScript source, and build tools in the production image. A typical Next.js project: single-stage = ~1.5 GB image, multi-stage = ~300 MB. Larger images mean slower deployments and a larger attack surface."
157
+ bad_example: |
158
+ # ❌ Single stage — devDependencies + source + build tools in production
159
+ FROM node:20
160
+ WORKDIR /app
161
+ COPY . . # includes src/, tests/, .env.example
162
+ RUN npm install # installs ALL deps including jest, ts-node, etc.
163
+ RUN npm run build
164
+ CMD ["node", "dist/index.js"]
165
+ good_example: |
166
+ # ✓ Multi-stage — production image has only what's needed to run
167
+ FROM node:20-alpine AS builder
168
+ RUN npm ci && npm run build
169
+ FROM node:20-alpine AS production
170
+ COPY package*.json ./
171
+ RUN npm ci --omit=dev # runtime deps only
172
+ COPY --from=builder /app/dist ./dist
173
+ - id: running_as_root
174
+ severity: warning
175
+ description: "Not switching to a non-root user before CMD — Docker containers run as root by default. If the application has an RCE vulnerability, the attacker has root-level access inside the container."
176
+ bad_example: |
177
+ # ❌ No USER instruction — process runs as root inside container
178
+ COPY --from=builder /app/dist ./dist
179
+ CMD ["node", "dist/index.js"]
180
+ good_example: |
181
+ # ✓ Switch to the built-in non-root node user
182
+ COPY --chown=node:node --from=builder /app/dist ./dist
183
+ USER node
184
+ CMD ["node", "dist/index.js"]
185
+ - id: using_latest_tag
186
+ severity: warning
187
+ description: "Using `FROM node:latest` — the `latest` tag is a moving target that resolves to a different version on each build. When a new Node.js major is released, `latest` jumps to it and your build may fail with breaking changes. Pin to a specific LTS version."
188
+ bad_example: |
189
+ # ❌ Non-deterministic — breaks when node:latest jumps to a new major
190
+ FROM node:latest
191
+ # Today: node 22. After next LTS release: could be node 24 — breaking
192
+ good_example: |
193
+ # ✓ Pinned to a specific LTS version — predictable builds
194
+ FROM node:20-alpine AS builder
195
+ FROM node:20-alpine AS production
196
+ # To upgrade, explicitly change the version and test
197
+ - id: copying_node_modules
198
+ severity: warning
199
+ description: "Running COPY . . before npm ci causes local node_modules to be copied into the image. node_modules built on macOS are not compatible with the Linux container — native addons fail, symlinks break, and the image includes dev dependencies."
200
+ bad_example: |
201
+ # ❌ COPY . . copies local node_modules into the image
202
+ FROM node:20-alpine
203
+ WORKDIR /app
204
+ COPY . . # copies node_modules from macOS host!
205
+ RUN npm ci # may use or be confused by the already-copied modules
206
+ CMD ["node", "dist/index.js"]
207
+ good_example: |
208
+ # ✓ Add node_modules to .dockerignore, copy package files first
209
+ # .dockerignore: node_modules, dist, .env
210
+ FROM node:20-alpine
211
+ WORKDIR /app
212
+ COPY package*.json ./ # package files only — no local node_modules
213
+ RUN npm ci # fresh install in the Linux container
214
+ COPY . . # source files only (node_modules excluded by .dockerignore)