@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.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +55 -0
- package/README.md +341 -0
- package/dist/analyzers/ast-parser.d.ts +3 -0
- package/dist/analyzers/ast-parser.js +305 -0
- package/dist/analyzers/ast-parser.js.map +1 -0
- package/dist/analyzers/dependency-graph.d.ts +2 -0
- package/dist/analyzers/dependency-graph.js +67 -0
- package/dist/analyzers/dependency-graph.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +56 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/file-walker.d.ts +3 -0
- package/dist/analyzers/file-walker.js +80 -0
- package/dist/analyzers/file-walker.js.map +1 -0
- package/dist/cli/context-runner.d.ts +1 -0
- package/dist/cli/context-runner.js +16 -0
- package/dist/cli/context-runner.js.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.js +217 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-runner.d.ts +25 -0
- package/dist/cli/init-runner.js +152 -0
- package/dist/cli/init-runner.js.map +1 -0
- package/dist/cli/scan-runner.d.ts +8 -0
- package/dist/cli/scan-runner.js +133 -0
- package/dist/cli/scan-runner.js.map +1 -0
- package/dist/formatters/plan-json.d.ts +2 -0
- package/dist/formatters/plan-json.js +4 -0
- package/dist/formatters/plan-json.js.map +1 -0
- package/dist/formatters/plan-markdown.d.ts +2 -0
- package/dist/formatters/plan-markdown.js +42 -0
- package/dist/formatters/plan-markdown.js.map +1 -0
- package/dist/formatters/plan-prompt.d.ts +4 -0
- package/dist/formatters/plan-prompt.js +5 -0
- package/dist/formatters/plan-prompt.js.map +1 -0
- package/dist/formatters/plan-terminal.d.ts +5 -0
- package/dist/formatters/plan-terminal.js +62 -0
- package/dist/formatters/plan-terminal.js.map +1 -0
- package/dist/generators/blueprint-renderer.d.ts +3 -0
- package/dist/generators/blueprint-renderer.js +27 -0
- package/dist/generators/blueprint-renderer.js.map +1 -0
- package/dist/generators/claudeWriter.d.ts +3 -0
- package/dist/generators/claudeWriter.js +9 -0
- package/dist/generators/claudeWriter.js.map +1 -0
- package/dist/generators/copilotWriter.d.ts +3 -0
- package/dist/generators/copilotWriter.js +11 -0
- package/dist/generators/copilotWriter.js.map +1 -0
- package/dist/generators/cursorWriter.d.ts +3 -0
- package/dist/generators/cursorWriter.js +14 -0
- package/dist/generators/cursorWriter.js.map +1 -0
- package/dist/generators/genericWriter.d.ts +3 -0
- package/dist/generators/genericWriter.js +9 -0
- package/dist/generators/genericWriter.js.map +1 -0
- package/dist/generators/template-context.d.ts +18 -0
- package/dist/generators/template-context.js +126 -0
- package/dist/generators/template-context.js.map +1 -0
- package/dist/generators/templateRenderer.d.ts +2 -0
- package/dist/generators/templateRenderer.js +19 -0
- package/dist/generators/templateRenderer.js.map +1 -0
- package/dist/generators/windsurfWriter.d.ts +3 -0
- package/dist/generators/windsurfWriter.js +14 -0
- package/dist/generators/windsurfWriter.js.map +1 -0
- package/dist/generators/writer-types.d.ts +11 -0
- package/dist/generators/writer-types.js +40 -0
- package/dist/generators/writer-types.js.map +1 -0
- package/dist/llm/claude-provider.d.ts +8 -0
- package/dist/llm/claude-provider.js +22 -0
- package/dist/llm/claude-provider.js.map +1 -0
- package/dist/llm/concern-classifier.d.ts +15 -0
- package/dist/llm/concern-classifier.js +61 -0
- package/dist/llm/concern-classifier.js.map +1 -0
- package/dist/llm/config.d.ts +11 -0
- package/dist/llm/config.js +120 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +8 -0
- package/dist/llm/ollama-provider.js +27 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +8 -0
- package/dist/llm/openai-provider.js +19 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompt-builder.d.ts +12 -0
- package/dist/llm/prompt-builder.js +132 -0
- package/dist/llm/prompt-builder.js.map +1 -0
- package/dist/llm/provider.d.ts +17 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/response-parser.d.ts +6 -0
- package/dist/llm/response-parser.js +128 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/planner/plan-generator.d.ts +7 -0
- package/dist/planner/plan-generator.js +275 -0
- package/dist/planner/plan-generator.js.map +1 -0
- package/dist/planner/plan-prompt-builder.d.ts +9 -0
- package/dist/planner/plan-prompt-builder.js +92 -0
- package/dist/planner/plan-prompt-builder.js.map +1 -0
- package/dist/planner/plan-response-parser.d.ts +7 -0
- package/dist/planner/plan-response-parser.js +21 -0
- package/dist/planner/plan-response-parser.js.map +1 -0
- package/dist/planner/plan-validator.d.ts +3 -0
- package/dist/planner/plan-validator.js +49 -0
- package/dist/planner/plan-validator.js.map +1 -0
- package/dist/reporters/scan-json.d.ts +13 -0
- package/dist/reporters/scan-json.js +26 -0
- package/dist/reporters/scan-json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.js +224 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/scoring/consistency-score.d.ts +3 -0
- package/dist/scoring/consistency-score.js +23 -0
- package/dist/scoring/consistency-score.js.map +1 -0
- package/dist/scoring/duplication-score.d.ts +3 -0
- package/dist/scoring/duplication-score.js +16 -0
- package/dist/scoring/duplication-score.js.map +1 -0
- package/dist/scoring/health-score.d.ts +4 -0
- package/dist/scoring/health-score.js +20 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/issue-builder.d.ts +4 -0
- package/dist/scoring/issue-builder.js +62 -0
- package/dist/scoring/issue-builder.js.map +1 -0
- package/dist/scoring/modularity-score.d.ts +3 -0
- package/dist/scoring/modularity-score.js +56 -0
- package/dist/scoring/modularity-score.js.map +1 -0
- package/dist/scoring/pattern-analysis.d.ts +3 -0
- package/dist/scoring/pattern-analysis.js +74 -0
- package/dist/scoring/pattern-analysis.js.map +1 -0
- package/dist/scoring/separation-score.d.ts +3 -0
- package/dist/scoring/separation-score.js +35 -0
- package/dist/scoring/separation-score.js.map +1 -0
- package/dist/skills/detector.d.ts +4 -0
- package/dist/skills/detector.js +104 -0
- package/dist/skills/detector.js.map +1 -0
- package/dist/skills/lister.d.ts +9 -0
- package/dist/skills/lister.js +35 -0
- package/dist/skills/lister.js.map +1 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +76 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/structure-check.d.ts +2 -0
- package/dist/skills/structure-check.js +37 -0
- package/dist/skills/structure-check.js.map +1 -0
- package/dist/skills/validator.d.ts +6 -0
- package/dist/skills/validator.js +229 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/types/analysis.d.ts +130 -0
- package/dist/types/analysis.js +41 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/concern.d.ts +48 -0
- package/dist/types/concern.js +16 -0
- package/dist/types/concern.js.map +1 -0
- package/dist/types/generation.d.ts +32 -0
- package/dist/types/generation.js +2 -0
- package/dist/types/generation.js.map +1 -0
- package/dist/types/issue.d.ts +12 -0
- package/dist/types/issue.js +2 -0
- package/dist/types/issue.js.map +1 -0
- package/dist/types/pattern.d.ts +15 -0
- package/dist/types/pattern.js +2 -0
- package/dist/types/pattern.js.map +1 -0
- package/dist/types/plan.d.ts +56 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/scan-output.d.ts +84 -0
- package/dist/types/scan-output.js +2 -0
- package/dist/types/scan-output.js.map +1 -0
- package/dist/types/scoring.d.ts +15 -0
- package/dist/types/scoring.js +2 -0
- package/dist/types/scoring.js.map +1 -0
- package/dist/types/skill.d.ts +97 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/agent-detector.d.ts +2 -0
- package/dist/utils/agent-detector.js +22 -0
- package/dist/utils/agent-detector.js.map +1 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +15 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +31 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/progress.d.ts +17 -0
- package/dist/utils/progress.js +48 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/thresholds.d.ts +6 -0
- package/dist/utils/thresholds.js +48 -0
- package/dist/utils/thresholds.js.map +1 -0
- package/package.json +63 -0
- package/skills/meta/general-js.skill.yaml +131 -0
- package/skills/patterns/clerk-auth.skill.yaml +349 -0
- package/skills/patterns/docker-deploy.skill.yaml +214 -0
- package/skills/patterns/drizzle.skill.yaml +277 -0
- package/skills/patterns/mongoose.skill.yaml +290 -0
- package/skills/patterns/nextauth.skill.yaml +308 -0
- package/skills/patterns/playwright-e2e.skill.yaml +265 -0
- package/skills/patterns/prisma.skill.yaml +255 -0
- package/skills/patterns/s3-storage.skill.yaml +235 -0
- package/skills/patterns/selenium-e2e.skill.yaml +276 -0
- package/skills/patterns/supabase-auth.skill.yaml +298 -0
- package/skills/patterns/supabase.skill.yaml +304 -0
- package/skills/patterns/vercel-deploy.skill.yaml +219 -0
- package/skills/patterns/vitest-testing.skill.yaml +262 -0
- package/skills/stacks/express-api.skill.yaml +155 -0
- package/skills/stacks/fastify-api.skill.yaml +119 -0
- package/skills/stacks/hono-api.skill.yaml +130 -0
- package/skills/stacks/nestjs.skill.yaml +135 -0
- package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
- package/skills/stacks/react-spa.skill.yaml +153 -0
- package/skills/stacks/vue-nuxt.skill.yaml +115 -0
- package/templates/architect-plan.md +139 -0
- 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
|
+
}
|