@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,304 @@
1
+ schema_version: "2.0.0"
2
+ id: supabase
3
+ name: "Supabase"
4
+ version: "2.0.0"
5
+ description: "Supabase as a backend — separate server/browser clients via @supabase/ssr, Row Level Security on every table, repository pattern for queries, and realtime subscriptions with cleanup."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - supabase
10
+ dependencies:
11
+ none:
12
+ - firebase
13
+ - "@firebase/app"
14
+ - mongodb
15
+ detection:
16
+ dependencies:
17
+ any:
18
+ - "@supabase/supabase-js"
19
+ - "@supabase/ssr"
20
+ source_indicators:
21
+ - "createClient("
22
+ - "supabase.from("
23
+ - "from '@supabase/supabase-js'"
24
+ - "from '@supabase/ssr'"
25
+ - "createServerClient"
26
+ - "createBrowserClient"
27
+ structure:
28
+ required_dirs:
29
+ - path: src/lib
30
+ purpose: "Supabase client factory files — supabase-server.ts for Server Components and API routes (reads session from cookies via @supabase/ssr), supabase-browser.ts for Client Components (createBrowserClient from @supabase/ssr). These two files must exist separately — mixing them causes session errors."
31
+ recommended_dirs:
32
+ - path: src/repositories
33
+ purpose: "Data access layer — one file per Supabase table (e.g., posts.repository.ts) containing all .from('posts') queries for that table. Services import repositories, never call supabase directly. This makes queries testable and keeps DB-specific code out of business logic."
34
+ - path: supabase
35
+ purpose: "Supabase project configuration — migrations/ (SQL files generated by supabase migrate), seed.sql (test data), and config.toml (local dev config). Run `supabase db push` to apply migrations."
36
+ separation:
37
+ rules:
38
+ - concern: server_client
39
+ belongs_in: src/lib
40
+ rule_text: "Create a server-side Supabase client in src/lib/supabase-server.ts using createServerClient from @supabase/ssr. This client reads the auth session from cookies and must be used in Server Components, API routes, and middleware. Call it as a function (not a singleton) so each request gets a fresh cookie snapshot."
41
+ example: |
42
+ // src/lib/supabase-server.ts
43
+ import { createServerClient } from '@supabase/ssr';
44
+ import { cookies } from 'next/headers';
45
+ import type { Database } from '@/types/supabase';
46
+
47
+ export async function createClient() {
48
+ const cookieStore = await cookies();
49
+ return createServerClient<Database>(
50
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
51
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
52
+ {
53
+ cookies: {
54
+ getAll: () => cookieStore.getAll(),
55
+ setAll: (cookiesToSet) => {
56
+ cookiesToSet.forEach(({ name, value, options }) =>
57
+ cookieStore.set(name, value, options)
58
+ );
59
+ },
60
+ },
61
+ }
62
+ );
63
+ }
64
+ indicators:
65
+ - "createServerClient"
66
+ - "@supabase/ssr"
67
+ - "cookies()"
68
+ - concern: browser_client
69
+ belongs_in: src/lib
70
+ rule_text: "Create a browser-side client in src/lib/supabase-browser.ts using createBrowserClient from @supabase/ssr. Export it as a module-level singleton — only one instance per browser page. Never import the browser client in Server Components or API routes; it cannot read server-side session cookies."
71
+ example: |
72
+ // src/lib/supabase-browser.ts
73
+ import { createBrowserClient } from '@supabase/ssr';
74
+ import type { Database } from '@/types/supabase';
75
+
76
+ // Module-level singleton — one client per browser session
77
+ export const supabase = createBrowserClient<Database>(
78
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
79
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
80
+ );
81
+ anti_indicators:
82
+ - "createBrowserClient"
83
+ - concern: rls_policies
84
+ belongs_in: supabase/migrations
85
+ rule_text: "Enable Row Level Security on every table before writing any queries. Write RLS policies that tie row access to auth.uid() — without policies, authenticated users can read and write each other's data. Use USING clause for SELECT/DELETE, WITH CHECK for INSERT/UPDATE."
86
+ example: |
87
+ -- supabase/migrations/[timestamp]_add_rls_policies.sql
88
+ -- Enable RLS on all tables
89
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
90
+ ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
91
+
92
+ -- Posts: full CRUD scoped to owner
93
+ CREATE POLICY "own posts" ON posts
94
+ FOR ALL
95
+ USING (auth.uid() = user_id)
96
+ WITH CHECK (auth.uid() = user_id);
97
+
98
+ -- Comments: anyone reads, only owner writes
99
+ CREATE POLICY "read comments" ON comments
100
+ FOR SELECT USING (true);
101
+ CREATE POLICY "own comments" ON comments
102
+ FOR ALL USING (auth.uid() = user_id);
103
+ indicators:
104
+ - "ENABLE ROW LEVEL SECURITY"
105
+ - "CREATE POLICY"
106
+ - "auth.uid()"
107
+ - "USING ("
108
+ - concern: data_access_layer
109
+ belongs_in: src/repositories
110
+ rule_text: "All Supabase queries go in repository files under src/repositories/. Services call repository functions — they never call supabase.from() directly. Repository files import the appropriate client (server or browser) based on where they run. This separation makes queries independently testable."
111
+ example: |
112
+ // src/repositories/posts.repository.ts
113
+ import { createClient } from '@/lib/supabase-server';
114
+
115
+ export async function listPostsByUser(userId: string) {
116
+ const supabase = await createClient();
117
+ const { data, error } = await supabase
118
+ .from('posts')
119
+ .select('id, title, created_at')
120
+ .eq('user_id', userId)
121
+ .order('created_at', { ascending: false });
122
+
123
+ if (error) throw new Error(`Failed to list posts: ${error.message}`);
124
+ return data;
125
+ }
126
+
127
+ export async function createPost(userId: string, title: string, content: string) {
128
+ const supabase = await createClient();
129
+ const { data, error } = await supabase
130
+ .from('posts')
131
+ .insert({ user_id: userId, title, content })
132
+ .select()
133
+ .single();
134
+
135
+ if (error) throw new Error(`Failed to create post: ${error.message}`);
136
+ return data;
137
+ }
138
+ indicators:
139
+ - "supabase.from("
140
+ - ".select("
141
+ - ".insert("
142
+ - ".update("
143
+ - concern: realtime_subscriptions
144
+ belongs_in: components
145
+ rule_text: "Set up Realtime subscriptions in useEffect in Client Components and unsubscribe in the cleanup function. A subscription that is not cleaned up leaks a WebSocket connection — in development with strict mode, this happens on every fast refresh."
146
+ example: |
147
+ // components/live-posts.tsx — Client Component
148
+ 'use client';
149
+ import { useEffect, useState } from 'react';
150
+ import { supabase } from '@/lib/supabase-browser';
151
+
152
+ export function LivePosts({ userId }: { userId: string }) {
153
+ const [posts, setPosts] = useState<Post[]>([]);
154
+
155
+ useEffect(() => {
156
+ const channel = supabase
157
+ .channel('posts-changes')
158
+ .on(
159
+ 'postgres_changes',
160
+ { event: '*', schema: 'public', table: 'posts', filter: `user_id=eq.${userId}` },
161
+ (payload) => {
162
+ if (payload.eventType === 'INSERT') setPosts(prev => [payload.new as Post, ...prev]);
163
+ if (payload.eventType === 'DELETE') setPosts(prev => prev.filter(p => p.id !== payload.old.id));
164
+ }
165
+ )
166
+ .subscribe();
167
+
168
+ // Cleanup: unsubscribe removes the WebSocket channel
169
+ return () => { supabase.removeChannel(channel); };
170
+ }, [userId]);
171
+
172
+ return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
173
+ }
174
+ indicators:
175
+ - "supabase.channel("
176
+ - "postgres_changes"
177
+ - "removeChannel"
178
+ - ".subscribe()"
179
+ patterns:
180
+ data_flow:
181
+ direction: "Client Component → Browser Client → RLS-enforced Query | Server Component → Server Client → RLS-enforced Query"
182
+ rules:
183
+ - "Server Components use the server client (createClient from supabase-server.ts) — reads session from cookies."
184
+ - "Client Components use the browser client (from supabase-browser.ts) — reads session from localStorage."
185
+ - "Service role key bypasses RLS — only use server-side for admin operations, never in browser code."
186
+ - "All .from() queries go through repository functions — services and components never call supabase.from() directly."
187
+ - "RLS is the primary security layer — policies enforce data isolation even if application code has bugs."
188
+ - "Realtime channels must be cleaned up in useEffect return function to prevent WebSocket leaks."
189
+ error_handling:
190
+ recommended: "Always destructure { data, error } from Supabase responses. If error is non-null, throw or return early — never access data when error is present. Log error.message and error.code for debugging. Code 42501 = RLS policy violation."
191
+ naming:
192
+ server_client: "src/lib/supabase-server.ts — createClient() factory using createServerClient from @supabase/ssr"
193
+ browser_client: "src/lib/supabase-browser.ts — singleton using createBrowserClient from @supabase/ssr"
194
+ repositories: "src/repositories/[resource].repository.ts — all .from('[resource]') queries for one table"
195
+ types: "src/types/supabase.ts — generated Database type from `supabase gen types typescript`"
196
+ anti_patterns:
197
+ - id: service_role_in_browser
198
+ severity: critical
199
+ description: "Using SUPABASE_SERVICE_ROLE_KEY in browser-accessible code or exposing it via NEXT_PUBLIC_ prefix. The service role key bypasses all RLS policies — any user who obtains it can read, write, or delete any row in any table."
200
+ bad_example: |
201
+ // ❌ Service role key in browser — bypasses every RLS policy
202
+ // Next.js NEXT_PUBLIC_ variables are embedded in client-side JS bundle
203
+ const supabase = createClient(
204
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
205
+ process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // exposed in browser
206
+ );
207
+ good_example: |
208
+ // ✓ Service role key only in server-side code (no NEXT_PUBLIC_ prefix)
209
+ // src/lib/supabase-admin.ts — only imported by API routes and Server Actions
210
+ import { createClient } from '@supabase/supabase-js';
211
+ export const supabaseAdmin = createClient(
212
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
213
+ process.env.SUPABASE_SERVICE_ROLE_KEY! // private, server-only
214
+ );
215
+ - id: tables_without_rls
216
+ severity: critical
217
+ description: "Creating tables without enabling Row Level Security. In Supabase, tables with RLS disabled and the anon key give every unauthenticated request full read/write access. Tables with RLS enabled but no policies default to DENY — but relying on 'deny by default' without explicit intent is fragile."
218
+ bad_example: |
219
+ -- ❌ No RLS — authenticated users can see and modify all rows
220
+ CREATE TABLE orders (
221
+ id uuid PRIMARY KEY,
222
+ user_id uuid REFERENCES auth.users(id),
223
+ total numeric
224
+ );
225
+ -- Missing: ALTER TABLE orders ENABLE ROW LEVEL SECURITY
226
+ -- Any authenticated user: SELECT * FROM orders → sees all orders
227
+ good_example: |
228
+ -- ✓ RLS enabled with explicit per-user policy
229
+ CREATE TABLE orders (
230
+ id uuid PRIMARY KEY,
231
+ user_id uuid REFERENCES auth.users(id) NOT NULL,
232
+ total numeric NOT NULL
233
+ );
234
+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
235
+ CREATE POLICY "own orders" ON orders
236
+ FOR ALL
237
+ USING (auth.uid() = user_id)
238
+ WITH CHECK (auth.uid() = user_id);
239
+ - id: browser_client_on_server
240
+ severity: warning
241
+ description: "Importing the browser Supabase client (from supabase-browser.ts) in Server Components or API routes. The browser client has no access to server-side cookie storage — supabase.auth.getUser() always returns null and all queries run as anonymous."
242
+ bad_example: |
243
+ // ❌ Browser client in a Server Component — no session
244
+ import { supabase } from '@/lib/supabase-browser';
245
+ // supabase.auth.getUser() → null (can't read server cookies)
246
+ const { data: posts } = await supabase.from('posts').select('*');
247
+ // RLS blocks this: auth.uid() = null → no rows returned (if policy exists)
248
+ // Or: all rows returned (if table has no RLS) — data leak!
249
+ good_example: |
250
+ // ✓ Server client in Server Component — reads session from cookies
251
+ import { createClient } from '@/lib/supabase-server';
252
+ const supabase = await createClient();
253
+ // auth.uid() = authenticated user → RLS applies correctly
254
+ const { data: posts } = await supabase.from('posts').select('*');
255
+ - id: unhandled_supabase_error
256
+ severity: warning
257
+ description: "Accessing the data field from a Supabase response without checking the error field first. When error is non-null, data is null or incomplete — accessing data properties on null causes runtime crashes."
258
+ bad_example: |
259
+ // ❌ Assuming success — data is null when error occurs
260
+ const { data } = await supabase.from('posts').select('*');
261
+ return data.map(post => post.title); // TypeError if data is null
262
+ good_example: |
263
+ // ✓ Always check error before using data
264
+ const { data, error } = await supabase.from('posts').select('*');
265
+ if (error) throw new Error(`Failed to fetch posts: ${error.message}`);
266
+ return data.map(post => post.title); // safe: data is non-null when error is null
267
+ - id: realtime_without_cleanup
268
+ severity: warning
269
+ description: "Setting up a Supabase Realtime subscription without cleaning up in useEffect's return function. Each leaked subscription holds an open WebSocket connection — 10 unmounted components without cleanup = 10 phantom WebSocket connections."
270
+ bad_example: |
271
+ // ❌ No cleanup — WebSocket connection leaks on unmount
272
+ useEffect(() => {
273
+ const channel = supabase
274
+ .channel('posts')
275
+ .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, handler)
276
+ .subscribe();
277
+ // Missing: return () => supabase.removeChannel(channel);
278
+ }, []);
279
+ good_example: |
280
+ // ✓ Cleanup removes the WebSocket channel on unmount
281
+ useEffect(() => {
282
+ const channel = supabase
283
+ .channel('posts')
284
+ .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, handler)
285
+ .subscribe();
286
+ return () => { supabase.removeChannel(channel); };
287
+ }, []);
288
+ - id: missing_generated_types
289
+ severity: warning
290
+ description: "Calling supabase.from('tablename') without using the generated Database type — loses all TypeScript type safety on the response. Column names, nullable fields, and return types are unknown, making the data layer impossible to refactor safely."
291
+ bad_example: |
292
+ // ❌ No Database type — from() returns 'any', no column type safety
293
+ import { createClient } from '@supabase/supabase-js';
294
+ const supabase = createClient(URL, KEY); // no generic type parameter
295
+ const { data } = await supabase.from('posts').select('*');
296
+ // data is any[] — typos in column names, wrong field shapes are undetected
297
+ good_example: |
298
+ // ✓ Generated types provide full type safety on queries and responses
299
+ // Generate with: supabase gen types typescript --local > src/types/supabase.ts
300
+ import type { Database } from '@/types/supabase';
301
+ import { createClient } from '@supabase/supabase-js';
302
+ const supabase = createClient<Database>(URL, KEY);
303
+ const { data } = await supabase.from('posts').select('id, title, created_at');
304
+ // data: { id: string; title: string; created_at: string }[] — fully typed
@@ -0,0 +1,219 @@
1
+ schema_version: "2.0.0"
2
+ id: vercel-deploy
3
+ name: "Vercel"
4
+ version: "2.0.0"
5
+ description: "Vercel deployment — dashboard-managed secrets, serverless function bundle limits, edge runtime with Web APIs, cache headers, and environment-based URL construction."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks: []
9
+ dependencies:
10
+ none: []
11
+ detection:
12
+ files:
13
+ - vercel.json
14
+ - .vercel
15
+ source_indicators:
16
+ - "VERCEL_URL"
17
+ - "vercel.json"
18
+ - "@vercel/og"
19
+ - "NEXT_PUBLIC_VERCEL"
20
+ structure:
21
+ required_dirs: []
22
+ recommended_dirs:
23
+ - path: .vercel
24
+ purpose: "Auto-generated Vercel project configuration — project.json links the local project to Vercel's project ID. Never edit manually; re-link with `vercel link` if corrupted."
25
+ separation:
26
+ rules:
27
+ - concern: env_variables
28
+ belongs_in: vercel.json
29
+ rule_text: "Store all secrets (API keys, database URLs, auth secrets) in Vercel's environment variable dashboard — not in vercel.json or committed .env files. Use `vercel env add NAME production` to set them. Reference values as process.env.NAME in code. Use NEXT_PUBLIC_ prefix only for values that are intentionally safe to expose in the browser bundle."
30
+ example: |
31
+ // vercel.json — build config only, never secrets
32
+ {
33
+ "buildCommand": "npm run build",
34
+ "outputDirectory": ".next",
35
+ "framework": "nextjs"
36
+ }
37
+ // Set secrets via CLI:
38
+ // vercel env add DATABASE_URL production
39
+ // vercel env add NEXTAUTH_SECRET production
40
+ // vercel env add STRIPE_SECRET_KEY production
41
+ // Pull to local .env for development:
42
+ // vercel env pull .env.local
43
+ anti_indicators:
44
+ - "\"env\":"
45
+ - "\"DATABASE_URL\":"
46
+ - concern: serverless_function_limits
47
+ belongs_in: app/api
48
+ rule_text: "Vercel serverless functions have a 50 MB bundle limit, 1024 MB RAM, and 10s default / 30s Pro timeout. For long-running operations, use maxDuration to extend the timeout explicitly. For heavy dependencies (Puppeteer, FFmpeg, Sharp), use an external service or Vercel's native image transformation instead."
49
+ example: |
50
+ // app/api/report/route.ts — extend timeout for heavy computation
51
+ import { NextRequest } from 'next/server';
52
+ export const maxDuration = 30; // seconds — Pro plan allows up to 300s
53
+
54
+ export async function POST(req: NextRequest) {
55
+ const body = await req.json();
56
+ const report = await generateReport(body); // slow DB-heavy operation
57
+ return Response.json(report);
58
+ }
59
+
60
+ // app/api/quick/route.ts — lightweight, no override needed (uses 10s default)
61
+ export async function GET() {
62
+ const data = await fetchLightData();
63
+ return Response.json(data, {
64
+ headers: { 'Cache-Control': 's-maxage=60, stale-while-revalidate=300' },
65
+ });
66
+ }
67
+ indicators:
68
+ - "maxDuration"
69
+ - "export const config"
70
+ - "runtime"
71
+ - concern: edge_runtime
72
+ belongs_in: middleware.ts
73
+ rule_text: "Use the Edge Runtime (export const runtime = 'edge') for middleware and low-latency routes that don't need Node.js APIs. Edge functions run globally at every CDN PoP — ideal for auth middleware, redirects, A/B testing, and geo-routing. Edge functions cannot use Node.js built-ins (fs, crypto from 'crypto', Buffer) — use the equivalent Web APIs."
74
+ example: |
75
+ // middleware.ts — edge runtime, runs before every request
76
+ import { NextResponse } from 'next/server';
77
+ import type { NextRequest } from 'next/server';
78
+
79
+ export function middleware(request: NextRequest) {
80
+ const token = request.cookies.get('session')?.value;
81
+ if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
82
+ return NextResponse.redirect(new URL('/login', request.url));
83
+ }
84
+ return NextResponse.next();
85
+ }
86
+
87
+ export const config = {
88
+ matcher: ['/dashboard/:path*', '/api/protected/:path*'],
89
+ };
90
+ indicators:
91
+ - "export const runtime = 'edge'"
92
+ - "config.matcher"
93
+ - "NextResponse.redirect"
94
+ - concern: cache_headers
95
+ belongs_in: app/api
96
+ rule_text: "Set Cache-Control headers on API routes that return stable or slowly-changing data. Without cache headers, every request hits your serverless function — Vercel's CDN cannot cache the response. Use s-maxage for CDN TTL and stale-while-revalidate for background refresh."
97
+ example: |
98
+ // app/api/products/route.ts — CDN-cached for 60s, stale-revalidate for 5min
99
+ export async function GET() {
100
+ const products = await getProducts();
101
+ return Response.json(products, {
102
+ headers: {
103
+ 'Cache-Control': 's-maxage=60, stale-while-revalidate=300',
104
+ // s-maxage: CDN caches for 60 seconds
105
+ // stale-while-revalidate: serve stale while revalidating in background
106
+ },
107
+ });
108
+ }
109
+
110
+ // For personalized responses — disable CDN caching
111
+ export async function GET(req: Request) {
112
+ const user = await getUser(req);
113
+ return Response.json(user, {
114
+ headers: { 'Cache-Control': 'private, no-store' },
115
+ });
116
+ }
117
+ indicators:
118
+ - "Cache-Control"
119
+ - "s-maxage"
120
+ - "stale-while-revalidate"
121
+ patterns:
122
+ data_flow:
123
+ direction: "Git Push → Vercel Build → Serverless (regional) or Edge (global) → CDN Cache → Response"
124
+ rules:
125
+ - "Production secrets are set in Vercel dashboard — never in committed files."
126
+ - "Edge functions run globally at CDN PoP — use for auth middleware, redirects, geo."
127
+ - "Serverless functions are regional — use for DB queries and heavy computation."
128
+ - "Set Cache-Control headers on stable API responses — Vercel CDN caches them globally."
129
+ - "Use VERCEL_URL for dynamic URL construction — never hardcode domain names."
130
+ error_handling:
131
+ recommended: "Check VERCEL_ENV ('production' | 'preview' | 'development') to distinguish environments. Use NEXT_PUBLIC_VERCEL_URL for browser-side URL construction."
132
+ naming:
133
+ config: "vercel.json at project root — build config only, never secrets"
134
+ api_routes: "app/api/[resource]/route.ts for serverless; export const runtime = 'edge' for edge functions"
135
+ env_management: "Secrets via 'vercel env add [NAME] [scope]'; local copy via 'vercel env pull .env.local'"
136
+ anti_patterns:
137
+ - id: secrets_in_vercel_json
138
+ severity: critical
139
+ description: "Putting secret values in vercel.json's env block. vercel.json is committed to the repository — secrets in it are visible to everyone with repo access and in git history permanently."
140
+ bad_example: |
141
+ // ❌ Secret in vercel.json — committed to git, leaked forever
142
+ {
143
+ "env": {
144
+ "DATABASE_URL": "postgresql://user:pass@db.example.com/prod",
145
+ "NEXTAUTH_SECRET": "my-production-secret"
146
+ }
147
+ }
148
+ good_example: |
149
+ // ✓ Set secrets via Vercel CLI — they go to the encrypted dashboard
150
+ // vercel env add DATABASE_URL production
151
+ // vercel env add NEXTAUTH_SECRET production
152
+ // vercel.json contains only non-secret build configuration
153
+ - id: heavy_deps_in_api_route
154
+ severity: warning
155
+ description: "Importing large native dependencies (Puppeteer, Sharp, FFmpeg) in serverless API routes — Puppeteer alone is ~350 MB (exceeds the 50 MB bundle limit). Results in build failures or extremely slow cold starts."
156
+ bad_example: |
157
+ // ❌ Puppeteer in a serverless function — 350 MB bundle → build fails
158
+ import puppeteer from 'puppeteer';
159
+ export async function GET() {
160
+ const browser = await puppeteer.launch();
161
+ const page = await browser.newPage();
162
+ // ... render screenshot
163
+ }
164
+ good_example: |
165
+ // ✓ Use Vercel's native OG image generation or an external screenshot service
166
+ import { ImageResponse } from 'next/og';
167
+ export const runtime = 'edge'; // OG works in edge runtime, tiny bundle
168
+ export async function GET() {
169
+ return new ImageResponse(<div>Hello</div>, { width: 1200, height: 630 });
170
+ }
171
+ - id: node_api_in_edge
172
+ severity: critical
173
+ description: "Using Node.js-only APIs (fs, path, Buffer, `import crypto from 'crypto'`) in edge functions or middleware. Edge runtime is based on V8 + Web APIs only — Node.js module imports throw at runtime."
174
+ bad_example: |
175
+ // ❌ Node.js crypto module in edge middleware — throws at runtime
176
+ import crypto from 'crypto'; // Node.js only
177
+ export function middleware(req: Request) {
178
+ const hash = crypto.createHash('sha256').update(req.url).digest('hex');
179
+ }
180
+ good_example: |
181
+ // ✓ Web Crypto API — available in edge runtime and browsers
182
+ export async function middleware(req: Request) {
183
+ const encoder = new TextEncoder();
184
+ const data = encoder.encode(req.url);
185
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
186
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
187
+ const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
188
+ }
189
+ - id: hardcoded_base_url
190
+ severity: warning
191
+ description: "Hardcoding domain names in API routes or server-side code instead of using VERCEL_URL. Vercel creates unique deployment URLs for every preview — hardcoded URLs break OAuth callbacks, email confirmation links, and API redirects in preview deployments."
192
+ bad_example: |
193
+ // ❌ Hardcoded URL — breaks in Vercel preview deployments
194
+ const callbackUrl = 'https://myapp.com/api/auth/callback';
195
+ const emailLink = `https://myapp.com/verify?token=${token}`;
196
+ good_example: |
197
+ // ✓ Dynamic base URL from environment
198
+ const baseUrl = process.env.VERCEL_URL
199
+ ? `https://${process.env.VERCEL_URL}`
200
+ : 'http://localhost:3000';
201
+ const callbackUrl = `${baseUrl}/api/auth/callback`;
202
+ const emailLink = `${baseUrl}/verify?token=${token}`;
203
+ - id: missing_cache_headers
204
+ severity: warning
205
+ description: "API routes that return stable or reference data without Cache-Control headers. Without headers, every request hits the serverless function — you pay per invocation and users experience higher latency. Even a 60-second CDN cache can reduce function invocations by 90% for popular endpoints."
206
+ bad_example: |
207
+ // ❌ No cache headers — every request invokes the function
208
+ export async function GET() {
209
+ const categories = await db.category.findMany(); // same data for hours
210
+ return Response.json(categories); // bypasses Vercel CDN entirely
211
+ }
212
+ good_example: |
213
+ // ✓ CDN caches for 60s, serves stale for 5 min while revalidating
214
+ export async function GET() {
215
+ const categories = await db.category.findMany();
216
+ return Response.json(categories, {
217
+ headers: { 'Cache-Control': 's-maxage=60, stale-while-revalidate=300' },
218
+ });
219
+ }