@intentsolutionsio/supabase-pack 1.0.0 → 1.0.3

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 (133) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +73 -47
  3. package/package.json +4 -4
  4. package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
  5. package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
  6. package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
  7. package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
  8. package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
  9. package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
  10. package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
  11. package/skills/supabase-architecture-variants/SKILL.md +395 -216
  12. package/skills/supabase-architecture-variants/references/errors.md +11 -0
  13. package/skills/supabase-architecture-variants/references/examples.md +12 -0
  14. package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
  15. package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
  16. package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
  17. package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
  18. package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
  19. package/skills/supabase-ci-integration/SKILL.md +315 -67
  20. package/skills/supabase-ci-integration/references/errors.md +10 -0
  21. package/skills/supabase-ci-integration/references/examples.md +36 -0
  22. package/skills/supabase-ci-integration/references/implementation.md +54 -0
  23. package/skills/supabase-common-errors/SKILL.md +320 -62
  24. package/skills/supabase-common-errors/references/errors.md +53 -0
  25. package/skills/supabase-common-errors/references/examples.md +23 -0
  26. package/skills/supabase-cost-tuning/SKILL.md +365 -131
  27. package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
  28. package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
  29. package/skills/supabase-cost-tuning/references/errors.md +11 -0
  30. package/skills/supabase-cost-tuning/references/examples.md +15 -0
  31. package/skills/supabase-data-handling/SKILL.md +378 -145
  32. package/skills/supabase-data-handling/references/errors.md +11 -0
  33. package/skills/supabase-data-handling/references/examples.md +27 -0
  34. package/skills/supabase-data-handling/references/implementation.md +223 -0
  35. package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
  36. package/skills/supabase-debug-bundle/SKILL.md +267 -73
  37. package/skills/supabase-debug-bundle/references/errors.md +12 -0
  38. package/skills/supabase-debug-bundle/references/examples.md +24 -0
  39. package/skills/supabase-debug-bundle/references/implementation.md +54 -0
  40. package/skills/supabase-deploy-integration/SKILL.md +258 -147
  41. package/skills/supabase-deploy-integration/references/errors.md +11 -0
  42. package/skills/supabase-deploy-integration/references/examples.md +21 -0
  43. package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
  44. package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
  45. package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
  46. package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
  47. package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
  48. package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
  49. package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
  50. package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
  51. package/skills/supabase-hello-world/SKILL.md +160 -54
  52. package/skills/supabase-incident-runbook/SKILL.md +453 -131
  53. package/skills/supabase-incident-runbook/references/errors.md +11 -0
  54. package/skills/supabase-incident-runbook/references/examples.md +10 -0
  55. package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
  56. package/skills/supabase-install-auth/SKILL.md +186 -50
  57. package/skills/supabase-install-auth/references/examples.md +102 -0
  58. package/skills/supabase-known-pitfalls/SKILL.md +411 -241
  59. package/skills/supabase-known-pitfalls/references/errors.md +11 -0
  60. package/skills/supabase-known-pitfalls/references/examples.md +12 -0
  61. package/skills/supabase-load-scale/SKILL.md +346 -217
  62. package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
  63. package/skills/supabase-load-scale/references/errors.md +11 -0
  64. package/skills/supabase-load-scale/references/examples.md +26 -0
  65. package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
  66. package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
  67. package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
  68. package/skills/supabase-local-dev-loop/SKILL.md +272 -73
  69. package/skills/supabase-local-dev-loop/references/errors.md +11 -0
  70. package/skills/supabase-local-dev-loop/references/examples.md +21 -0
  71. package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
  72. package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
  73. package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
  74. package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
  75. package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
  76. package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
  77. package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
  78. package/skills/supabase-multi-env-setup/SKILL.md +393 -152
  79. package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
  80. package/skills/supabase-multi-env-setup/references/errors.md +11 -0
  81. package/skills/supabase-multi-env-setup/references/examples.md +11 -0
  82. package/skills/supabase-observability/SKILL.md +318 -196
  83. package/skills/supabase-observability/references/alert-configuration.md +40 -0
  84. package/skills/supabase-observability/references/errors.md +11 -0
  85. package/skills/supabase-observability/references/examples.md +13 -0
  86. package/skills/supabase-observability/references/metrics-collection.md +65 -0
  87. package/skills/supabase-performance-tuning/SKILL.md +304 -160
  88. package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
  89. package/skills/supabase-performance-tuning/references/errors.md +11 -0
  90. package/skills/supabase-performance-tuning/references/examples.md +13 -0
  91. package/skills/supabase-policy-guardrails/SKILL.md +248 -221
  92. package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
  93. package/skills/supabase-policy-guardrails/references/errors.md +11 -0
  94. package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
  95. package/skills/supabase-policy-guardrails/references/examples.md +10 -0
  96. package/skills/supabase-prod-checklist/SKILL.md +474 -84
  97. package/skills/supabase-prod-checklist/references/errors.md +63 -0
  98. package/skills/supabase-prod-checklist/references/examples.md +153 -0
  99. package/skills/supabase-prod-checklist/references/implementation.md +113 -0
  100. package/skills/supabase-rate-limits/SKILL.md +311 -98
  101. package/skills/supabase-rate-limits/references/errors.md +11 -0
  102. package/skills/supabase-rate-limits/references/examples.md +46 -0
  103. package/skills/supabase-rate-limits/references/implementation.md +66 -0
  104. package/skills/supabase-reference-architecture/SKILL.md +249 -182
  105. package/skills/supabase-reference-architecture/references/errors.md +29 -0
  106. package/skills/supabase-reference-architecture/references/examples.md +116 -0
  107. package/skills/supabase-reference-architecture/references/key-components.md +244 -0
  108. package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
  109. package/skills/supabase-reliability-patterns/SKILL.md +229 -234
  110. package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
  111. package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
  112. package/skills/supabase-reliability-patterns/references/errors.md +11 -0
  113. package/skills/supabase-reliability-patterns/references/examples.md +11 -0
  114. package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
  115. package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
  116. package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
  117. package/skills/supabase-sdk-patterns/SKILL.md +388 -99
  118. package/skills/supabase-sdk-patterns/references/errors.md +11 -0
  119. package/skills/supabase-sdk-patterns/references/examples.md +45 -0
  120. package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
  121. package/skills/supabase-security-basics/SKILL.md +282 -102
  122. package/skills/supabase-security-basics/references/errors.md +10 -0
  123. package/skills/supabase-security-basics/references/examples.md +70 -0
  124. package/skills/supabase-security-basics/references/implementation.md +39 -0
  125. package/skills/supabase-upgrade-migration/SKILL.md +248 -66
  126. package/skills/supabase-upgrade-migration/references/errors.md +10 -0
  127. package/skills/supabase-upgrade-migration/references/examples.md +51 -0
  128. package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
  129. package/skills/supabase-webhooks-events/SKILL.md +412 -138
  130. package/skills/supabase-webhooks-events/references/errors.md +55 -0
  131. package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
  132. package/skills/supabase-webhooks-events/references/examples.md +133 -0
  133. package/skills/supabase-webhooks-events/references/signature-verification.md +165 -0
@@ -1,334 +1,504 @@
1
1
  ---
2
2
  name: supabase-known-pitfalls
3
- description: |
4
- Identify and avoid Supabase anti-patterns and common integration mistakes.
5
- Use when reviewing Supabase code for issues, onboarding new developers,
6
- or auditing existing Supabase integrations for best practices violations.
3
+ description: 'Avoid and fix the most common Supabase mistakes: exposing service_role
4
+ key
5
+
6
+ in client bundles, forgetting to enable RLS, not using connection pooling
7
+
8
+ in serverless, .single() throwing on empty results, missing .select() after
9
+
10
+ insert/update, not destructuring { data, error }, creating multiple client
11
+
12
+ instances, and not using generated types.
13
+
14
+ Use when reviewing Supabase code, onboarding developers, auditing an
15
+
16
+ existing project, or debugging unexpected behavior.
17
+
7
18
  Trigger with phrases like "supabase mistakes", "supabase anti-patterns",
8
- "supabase pitfalls", "supabase what not to do", "supabase code review".
19
+
20
+ "supabase pitfalls", "supabase code review", "supabase gotchas",
21
+
22
+ "supabase debugging", "what not to do supabase", "supabase common errors".
23
+
24
+ '
9
25
  allowed-tools: Read, Grep
10
26
  version: 1.0.0
11
27
  license: MIT
12
28
  author: Jeremy Longshore <jeremy@intentsolutions.io>
29
+ tags:
30
+ - saas
31
+ - supabase
32
+ - anti-patterns
33
+ - code-review
34
+ - debugging
35
+ - security
36
+ - pitfalls
37
+ compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
13
38
  ---
14
-
15
39
  # Supabase Known Pitfalls
16
40
 
17
41
  ## Overview
18
- Common mistakes and anti-patterns when integrating with Supabase.
42
+
43
+ The twelve most common Supabase mistakes, ranked by severity: **security** (service_role exposure, missing RLS, permissive policies), **data integrity** (ignoring `{ data, error }`, missing `.select()` after mutations, `.single()` on optional results), **performance** (`select('*')`, N+1 queries, missing FK indexes, synchronous auth checks), and **maintainability** (no generated types, multiple client instances, hardcoded connection strings). Each pitfall shows the broken code, explains why it fails, and provides the correct pattern using `createClient` from `@supabase/supabase-js`.
19
44
 
20
45
  ## Prerequisites
21
- - Access to Supabase codebase for review
22
- - Understanding of async/await patterns
23
- - Knowledge of security best practices
24
- - Familiarity with rate limiting concepts
25
46
 
26
- ## Pitfall #1: Synchronous API Calls in Request Path
47
+ - Access to a Supabase project codebase for review
48
+ - `@supabase/supabase-js` v2+ installed
49
+ - Basic understanding of Row Level Security (RLS)
27
50
 
28
- ### Anti-Pattern
29
- ```typescript
30
- // User waits for Supabase API call
31
- app.post('/checkout', async (req, res) => {
32
- const payment = await supabaseClient.processPayment(req.body); // 2-5s latency
33
- const notification = await supabaseClient.sendEmail(payment); // Another 1-2s
34
- res.json({ success: true }); // User waited 3-7s
35
- });
36
- ```
37
-
38
- ### ✅ Better Approach
39
- ```typescript
40
- // Return immediately, process async
41
- app.post('/checkout', async (req, res) => {
42
- const jobId = await queue.enqueue('process-checkout', req.body);
43
- res.json({ jobId, status: 'processing' }); // 50ms response
44
- });
45
-
46
- // Background job
47
- async function processCheckout(data) {
48
- const payment = await supabaseClient.processPayment(data);
49
- await supabaseClient.sendEmail(payment);
50
- }
51
- ```
51
+ ## Step 1 — Security Pitfalls (Critical)
52
52
 
53
- ---
53
+ These mistakes can expose all your data to any user with browser dev tools.
54
54
 
55
- ## Pitfall #2: Not Handling Rate Limits
55
+ ### Pitfall 1: Exposing service_role Key in Client Code
56
56
 
57
- ### ❌ Anti-Pattern
58
57
  ```typescript
59
- // Blast requests, crash on 429
60
- for (const item of items) {
61
- await supabaseClient.process(item); // Will hit rate limit
62
- }
58
+ // BAD: service_role key in a NEXT_PUBLIC_ variable — shipped to every browser
59
+ import { createClient } from '@supabase/supabase-js'
60
+
61
+ const supabase = createClient(
62
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
63
+ process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // CATASTROPHIC
64
+ )
65
+ // This key bypasses ALL RLS. Anyone can:
66
+ // - Read every row in every table
67
+ // - Delete the entire database
68
+ // - Create admin users
69
+ // - Access every file in storage
70
+
71
+ // CORRECT: anon key on client, service_role only on server
72
+ // Client (browser):
73
+ const supabase = createClient(
74
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
75
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // respects RLS
76
+ )
77
+
78
+ // Server only (API routes, server actions):
79
+ const supabaseAdmin = createClient(
80
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
81
+ process.env.SUPABASE_SERVICE_ROLE_KEY!, // NO NEXT_PUBLIC_ prefix
82
+ { auth: { autoRefreshToken: false, persistSession: false } }
83
+ )
63
84
  ```
64
85
 
65
- ### ✅ Better Approach
66
- ```typescript
67
- import pLimit from 'p-limit';
68
-
69
- const limit = pLimit(5); // Max 5 concurrent
70
- const rateLimiter = new RateLimiter({ tokensPerSecond: 10 });
86
+ **Detection:**
71
87
 
72
- for (const item of items) {
73
- await rateLimiter.acquire();
74
- await limit(() => supabaseClient.process(item));
75
- }
88
+ ```bash
89
+ # Find service_role references in client-side files
90
+ grep -rn 'SERVICE_ROLE' --include="*.tsx" --include="*.jsx" --include="*.ts" src/ app/ components/ pages/
91
+ # Find NEXT_PUBLIC_ + SERVICE_ROLE combination
92
+ grep -rn 'NEXT_PUBLIC.*SERVICE_ROLE' .env* *.ts *.tsx
76
93
  ```
77
94
 
78
- ---
79
-
80
- ## Pitfall #3: Leaking API Keys
95
+ ### Pitfall 2: Tables Without RLS Enabled
81
96
 
82
- ### ❌ Anti-Pattern
83
- ```typescript
84
- // In frontend code (visible to users!)
85
- const client = new SupabaseClient({
86
- apiKey: 'sk_live_ACTUAL_KEY_HERE', // Anyone can see this
87
- });
97
+ ```sql
98
+ -- BAD: table created without enabling RLS
99
+ CREATE TABLE public.medical_records (
100
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
101
+ patient_id uuid REFERENCES auth.users(id),
102
+ diagnosis text,
103
+ ssn text -- PII fully exposed to anyone with the anon key!
104
+ );
105
+ -- With RLS disabled, the anon key can read EVERY row via the PostgREST API
106
+
107
+ -- CORRECT: always enable RLS immediately after CREATE TABLE
108
+ CREATE TABLE public.medical_records (
109
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
110
+ patient_id uuid REFERENCES auth.users(id),
111
+ diagnosis text,
112
+ ssn text
113
+ );
114
+ ALTER TABLE public.medical_records ENABLE ROW LEVEL SECURITY;
88
115
 
89
- // In git history
90
- git commit -m "add API key" // Exposed forever
116
+ -- Then add policies for legitimate access
117
+ CREATE POLICY "patients_read_own" ON public.medical_records
118
+ FOR SELECT USING (patient_id = auth.uid());
91
119
  ```
92
120
 
93
- ### ✅ Better Approach
94
- ```typescript
95
- // Backend only, environment variable
96
- const client = new SupabaseClient({
97
- apiKey: process.env.SUPABASE_API_KEY,
98
- });
99
-
100
- // Use .gitignore
101
- .env
102
- .env.local
103
- .env.*.local
121
+ **Detection:**
122
+
123
+ ```sql
124
+ -- Find all tables without RLS (run in SQL Editor)
125
+ SELECT schemaname, tablename
126
+ FROM pg_tables
127
+ WHERE schemaname = 'public'
128
+ AND rowsecurity = false
129
+ AND tablename NOT LIKE '\_%';
104
130
  ```
105
131
 
106
- ---
132
+ ### Pitfall 3: Overly Permissive RLS Policies
107
133
 
108
- ## Pitfall #4: Ignoring Idempotency
134
+ ```sql
135
+ -- BAD: lets any authenticated user read ALL messages
136
+ CREATE POLICY "anyone_can_read" ON public.messages
137
+ FOR SELECT USING (auth.uid() IS NOT NULL);
138
+ -- Every logged-in user sees every other user's private messages
109
139
 
110
- ### Anti-Pattern
111
- ```typescript
112
- // Network error on response = duplicate charge!
113
- try {
114
- await supabaseClient.charge(order);
115
- } catch (error) {
116
- if (error.code === 'NETWORK_ERROR') {
117
- await supabaseClient.charge(order); // Charged twice!
118
- }
119
- }
120
- ```
140
+ -- BAD: lets any authenticated user update ANY row
141
+ CREATE POLICY "anyone_can_update" ON public.profiles
142
+ FOR UPDATE USING (auth.uid() IS NOT NULL);
143
+ -- Users can edit each other's profiles
121
144
 
122
- ### Better Approach
123
- ```typescript
124
- const idempotencyKey = `order-${order.id}-${Date.now()}`;
145
+ -- CORRECT: scope to the user's own data
146
+ CREATE POLICY "read_own_messages" ON public.messages
147
+ FOR SELECT USING (
148
+ sender_id = auth.uid() OR recipient_id = auth.uid()
149
+ );
125
150
 
126
- await supabaseClient.charge(order, {
127
- idempotencyKey, // Safe to retry
128
- });
151
+ CREATE POLICY "update_own_profile" ON public.profiles
152
+ FOR UPDATE USING (id = auth.uid());
129
153
  ```
130
154
 
131
- ---
132
-
133
- ## Pitfall #5: Not Validating Webhooks
155
+ ### Pitfall 4: Not Using Connection Pooling in Serverless
134
156
 
135
- ### ❌ Anti-Pattern
136
157
  ```typescript
137
- // Trust any incoming request
138
- app.post('/webhook', (req, res) => {
139
- processWebhook(req.body); // Attacker can send fake events
140
- res.sendStatus(200);
141
- });
158
+ // BAD: direct connection string in a serverless function
159
+ // Each Lambda/Edge invocation opens a new connection — exhausts pool in minutes
160
+ const connectionString = 'postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres'
161
+
162
+ // CORRECT: use the pooled connection string (Supavisor, port 6543)
163
+ const connectionString = 'postgresql://postgres.xxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
164
+ // Transaction mode: shares connections across requests
165
+ // Required for serverless (Vercel, Netlify, Cloudflare Workers, AWS Lambda)
142
166
  ```
143
167
 
144
- ### Better Approach
145
- ```typescript
146
- app.post('/webhook',
147
- express.raw({ type: 'application/json' }),
148
- (req, res) => {
149
- const signature = req.headers['x-supabase-signature'];
150
- if (!verifySupabaseSignature(req.body, signature)) {
151
- return res.sendStatus(401);
152
- }
153
- processWebhook(JSON.parse(req.body));
154
- res.sendStatus(200);
155
- }
156
- );
157
- ```
168
+ ## Step 2 — Data Integrity Pitfalls (High)
158
169
 
159
- ---
170
+ These mistakes cause silent data loss, null pointer errors, and inconsistent state.
160
171
 
161
- ## Pitfall #6: Missing Error Handling
172
+ ### Pitfall 5: Not Handling { data, error }
162
173
 
163
- ### ❌ Anti-Pattern
164
174
  ```typescript
165
- // Crashes on any error
166
- const result = await supabaseClient.get(id);
167
- console.log(result.data.nested.value); // TypeError if missing
168
- ```
169
-
170
- ### Better Approach
171
- ```typescript
172
- try {
173
- const result = await supabaseClient.get(id);
174
- console.log(result?.data?.nested?.value ?? 'default');
175
- } catch (error) {
176
- if (error instanceof SupabaseNotFoundError) {
177
- return null;
178
- }
179
- if (error instanceof SupabaseRateLimitError) {
180
- await sleep(error.retryAfter);
181
- return this.get(id); // Retry
182
- }
183
- throw error; // Rethrow unknown errors
175
+ import { createClient } from '@supabase/supabase-js'
176
+ const supabase = createClient(url, key)
177
+
178
+ // BAD: destructuring only data — errors silently ignored
179
+ const { data } = await supabase.from('orders').insert(order).select().single()
180
+ console.log(data.id) // TypeError: Cannot read property 'id' of null
181
+ // The insert failed (maybe RLS blocked it), data is null, error has the reason
182
+
183
+ // CORRECT: always check error before using data
184
+ const { data, error } = await supabase.from('orders').insert(order).select().single()
185
+ if (error) {
186
+ console.error('Insert failed:', error.code, error.message, error.details)
187
+ throw new Error(`Order creation failed: ${error.message}`)
184
188
  }
189
+ // Now data is guaranteed to be non-null
190
+ console.log(data.id)
185
191
  ```
186
192
 
187
- ---
188
-
189
- ## Pitfall #7: Hardcoding Configuration
193
+ ### Pitfall 6: Missing .select() After Insert/Update/Upsert
190
194
 
191
- ### ❌ Anti-Pattern
192
195
  ```typescript
193
- const client = new SupabaseClient({
194
- timeout: 5000, // Too short for some operations
195
- baseUrl: 'https://api.supabase.com', // Can't change for staging
196
- });
196
+ import { createClient } from '@supabase/supabase-js'
197
+ const supabase = createClient(url, key)
198
+
199
+ // BAD: insert without .select() returns NO data
200
+ const { data } = await supabase.from('todos').insert({ title: 'Buy milk' })
201
+ console.log(data) // null! Not the inserted row.
202
+ // Supabase mutations return null by default (like SQL INSERT without RETURNING)
203
+
204
+ // CORRECT: chain .select() to get the inserted/updated row back
205
+ const { data, error } = await supabase
206
+ .from('todos')
207
+ .insert({ title: 'Buy milk' })
208
+ .select('id, title, is_complete, created_at') // like SQL RETURNING
209
+ .single()
210
+
211
+ if (error) throw new Error(`Insert failed: ${error.message}`)
212
+ console.log(data) // { id: '...', title: 'Buy milk', is_complete: false, ... }
197
213
  ```
198
214
 
199
- ### Better Approach
215
+ ### Pitfall 7: .single() on Empty or Multiple Results
216
+
200
217
  ```typescript
201
- const client = new SupabaseClient({
202
- timeout: parseInt(process.env.SUPABASE_TIMEOUT || '30000'),
203
- baseUrl: process.env.SUPABASE_BASE_URL || 'https://api.supabase.com',
204
- });
218
+ import { createClient } from '@supabase/supabase-js'
219
+ const supabase = createClient(url, key)
220
+
221
+ // BAD: .single() throws PGRST116 when no rows match
222
+ const { data, error } = await supabase
223
+ .from('profiles')
224
+ .select('id, username, avatar_url')
225
+ .eq('username', searchTerm)
226
+ .single()
227
+ // error: { code: 'PGRST116', message: 'JSON object requested, multiple (or no) rows returned' }
228
+ // This is an ERROR, not just null — it breaks your flow
229
+
230
+ // BAD: .single() also throws when MULTIPLE rows match (PGRST200)
231
+
232
+ // CORRECT: use .maybeSingle() for 0-or-1 results
233
+ const { data, error } = await supabase
234
+ .from('profiles')
235
+ .select('id, username, avatar_url')
236
+ .eq('username', searchTerm)
237
+ .maybeSingle()
238
+ // data is null if no match (no error thrown)
239
+ // data is the row if exactly one match
240
+ // error only if 2+ rows match
241
+
242
+ // RULE OF THUMB:
243
+ // .single() — use ONLY when you KNOW exactly 1 row exists (e.g., by primary key)
244
+ // .maybeSingle() — use when 0 or 1 rows might match (lookups by unique field)
245
+ // neither — use when you expect an array of results
205
246
  ```
206
247
 
207
- ---
248
+ ## Step 3 — Performance and Maintainability Pitfalls (Medium/Low)
208
249
 
209
- ## Pitfall #8: Not Implementing Circuit Breaker
250
+ ### Pitfall 8: select('*') Everywhere
210
251
 
211
- ### ❌ Anti-Pattern
212
252
  ```typescript
213
- // When Supabase is down, every request hangs
214
- for (const user of users) {
215
- await supabaseClient.sync(user); // All timeout sequentially
216
- }
253
+ import { createClient } from '@supabase/supabase-js'
254
+ const supabase = createClient(url, key)
255
+
256
+ // BAD: fetches ALL columns including large text/jsonb/bytea fields
257
+ const { data } = await supabase.from('posts').select('*')
258
+ // Problems:
259
+ // 1. Transfers unnecessary data (slower, more bandwidth)
260
+ // 2. May leak sensitive columns (SSN, internal notes, hashed passwords)
261
+ // 3. No TypeScript autocompletion — type is too broad
262
+ // 4. Cannot benefit from covering indexes
263
+
264
+ // CORRECT: specify only the columns you need
265
+ const { data } = await supabase
266
+ .from('posts')
267
+ .select('id, title, slug, excerpt, published_at, author:profiles(name, avatar_url)')
268
+ // Benefits: smaller payload, typed results, index-friendly, no data leakage
217
269
  ```
218
270
 
219
- ### Better Approach
220
- ```typescript
221
- import CircuitBreaker from 'opossum';
271
+ ### Pitfall 9: N+1 Query Pattern
222
272
 
223
- const breaker = new CircuitBreaker(supabaseClient.sync, {
224
- timeout: 10000,
225
- errorThresholdPercentage: 50,
226
- resetTimeout: 30000,
227
- });
228
-
229
- // Fails fast when circuit is open
230
- for (const user of users) {
231
- await breaker.fire(user).catch(handleFailure);
273
+ ```typescript
274
+ import { createClient } from '@supabase/supabase-js'
275
+ const supabase = createClient(url, key)
276
+
277
+ // BAD: 1 query to get projects + N queries to get tasks per project
278
+ const { data: projects } = await supabase.from('projects').select('id, name')
279
+
280
+ for (const project of projects ?? []) {
281
+ const { data: tasks } = await supabase
282
+ .from('tasks')
283
+ .select('id, title, status')
284
+ .eq('project_id', project.id)
285
+ project.tasks = tasks // N additional queries!
232
286
  }
287
+ // Total: 1 + N queries (if you have 50 projects, that's 51 queries)
288
+
289
+ // CORRECT: use PostgREST embedded joins (single query)
290
+ const { data } = await supabase
291
+ .from('projects')
292
+ .select(`
293
+ id, name,
294
+ tasks (id, title, status)
295
+ `)
296
+ // Total: 1 query with automatic JOIN
297
+ // PostgREST detects the foreign key and embeds the related data
298
+
299
+ // ALSO CORRECT: batch with .in() for non-FK relationships
300
+ const projectIds = projects?.map(p => p.id) ?? []
301
+ const { data: allTasks } = await supabase
302
+ .from('tasks')
303
+ .select('id, title, status, project_id')
304
+ .in('project_id', projectIds)
305
+ // Total: 2 queries regardless of N
233
306
  ```
234
307
 
235
- ---
236
-
237
- ## Pitfall #9: Logging Sensitive Data
308
+ ### Pitfall 10: Missing Indexes on Foreign Key Columns
238
309
 
239
- ### ❌ Anti-Pattern
240
- ```typescript
241
- console.log('Request:', JSON.stringify(request)); // Logs API key, PII
242
- console.log('User:', user); // Logs email, phone
310
+ ```sql
311
+ -- BAD: foreign key without index
312
+ CREATE TABLE public.comments (
313
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
314
+ post_id uuid REFERENCES public.posts(id), -- no index!
315
+ author_id uuid REFERENCES auth.users(id), -- no index!
316
+ body text,
317
+ created_at timestamptz DEFAULT now()
318
+ );
319
+ -- Every query filtering by post_id or author_id does a sequential scan
320
+ -- RLS policies checking these columns also become slow
321
+
322
+ -- CORRECT: always index foreign key columns
323
+ CREATE TABLE public.comments (
324
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
325
+ post_id uuid REFERENCES public.posts(id),
326
+ author_id uuid REFERENCES auth.users(id),
327
+ body text,
328
+ created_at timestamptz DEFAULT now()
329
+ );
330
+ CREATE INDEX idx_comments_post_id ON public.comments(post_id);
331
+ CREATE INDEX idx_comments_author_id ON public.comments(author_id);
243
332
  ```
244
333
 
245
- ### ✅ Better Approach
246
- ```typescript
247
- const redacted = {
248
- ...request,
249
- apiKey: '[REDACTED]',
250
- user: { id: user.id }, // Only non-sensitive fields
251
- };
252
- console.log('Request:', JSON.stringify(redacted));
334
+ **Detection:**
335
+
336
+ ```sql
337
+ -- Find foreign keys without indexes
338
+ SELECT
339
+ tc.table_name,
340
+ kcu.column_name,
341
+ 'CREATE INDEX idx_' || tc.table_name || '_' || kcu.column_name
342
+ || ' ON public.' || tc.table_name || '(' || kcu.column_name || ');' AS fix
343
+ FROM information_schema.table_constraints tc
344
+ JOIN information_schema.key_column_usage kcu
345
+ ON tc.constraint_name = kcu.constraint_name
346
+ LEFT JOIN pg_indexes pi
347
+ ON pi.tablename = tc.table_name
348
+ AND pi.indexdef LIKE '%' || kcu.column_name || '%'
349
+ WHERE tc.constraint_type = 'FOREIGN KEY'
350
+ AND tc.table_schema = 'public'
351
+ AND pi.indexname IS NULL;
253
352
  ```
254
353
 
255
- ---
256
-
257
- ## Pitfall #10: No Graceful Degradation
354
+ ### Pitfall 11: Creating Multiple Client Instances
258
355
 
259
- ### ❌ Anti-Pattern
260
356
  ```typescript
261
- // Entire feature broken if Supabase is down
262
- const recommendations = await supabaseClient.getRecommendations(userId);
263
- return renderPage({ recommendations }); // Page crashes
357
+ // BAD: new client in every file — wastes memory, breaks auth state
358
+ // utils/auth.ts
359
+ import { createClient } from '@supabase/supabase-js'
360
+ const supabase = createClient(url, key) // instance 1
361
+
362
+ // utils/data.ts
363
+ import { createClient } from '@supabase/supabase-js'
364
+ const supabase = createClient(url, key) // instance 2 — separate auth session!
365
+
366
+ // components/Profile.tsx
367
+ import { createClient } from '@supabase/supabase-js'
368
+ const supabase = createClient(url, key) // instance 3 — yet another session
369
+
370
+ // Problems:
371
+ // - Auth state is not shared between instances
372
+ // - Realtime subscriptions multiply
373
+ // - Memory overhead from duplicate GoTrue instances
374
+ // - Session refresh happens 3x unnecessarily
375
+
376
+ // CORRECT: singleton pattern — one instance, imported everywhere
377
+ // lib/supabase.ts
378
+ import { createClient } from '@supabase/supabase-js'
379
+ import type { Database } from './database.types'
380
+
381
+ export const supabase = createClient<Database>(
382
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
383
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
384
+ )
385
+
386
+ // Every file imports the same instance:
387
+ // import { supabase } from '@/lib/supabase'
264
388
  ```
265
389
 
266
- ### Better Approach
390
+ ### Pitfall 12: Not Using Generated Types
391
+
267
392
  ```typescript
268
- let recommendations;
269
- try {
270
- recommendations = await supabaseClient.getRecommendations(userId);
271
- } catch (error) {
272
- recommendations = await getFallbackRecommendations(userId);
273
- reportDegradedService('supabase', error);
393
+ // BAD: manual types that drift from the actual database schema
394
+ interface Todo {
395
+ id: number // wrong! Supabase uses uuid by default
396
+ title: string
397
+ done: boolean // wrong! Column is actually called is_complete
398
+ createdAt: string // wrong! Column is actually called created_at
274
399
  }
275
- return renderPage({ recommendations, degraded: !recommendations });
276
- ```
277
400
 
278
- ---
401
+ // The compiler doesn't catch any of these mismatches.
402
+ // You get runtime errors instead of compile-time errors.
403
+
404
+ // CORRECT: generate types from your database schema
405
+ // Step 1: Generate
406
+ // npx supabase gen types typescript --linked > lib/database.types.ts
407
+
408
+ // Step 2: Use the generated types
409
+ import type { Database } from './database.types'
279
410
 
280
- ## Instructions
411
+ type Todo = Database['public']['Tables']['todos']['Row']
412
+ // { id: string, title: string, is_complete: boolean, created_at: string, user_id: string }
281
413
 
282
- ### Step 1: Review for Anti-Patterns
283
- Scan codebase for each pitfall pattern.
414
+ type TodoInsert = Database['public']['Tables']['todos']['Insert']
415
+ // { title: string, user_id?: string, is_complete?: boolean }
284
416
 
285
- ### Step 2: Prioritize Fixes
286
- Address security issues first, then performance.
417
+ // Step 3: Pass the Database type to createClient
418
+ import { createClient } from '@supabase/supabase-js'
287
419
 
288
- ### Step 3: Implement Better Approach
289
- Replace anti-patterns with recommended patterns.
420
+ const supabase = createClient<Database>(url, key)
290
421
 
291
- ### Step 4: Add Prevention
292
- Set up linting and CI checks to prevent recurrence.
422
+ // Now all queries are fully typed:
423
+ const { data } = await supabase.from('todos').select('id, title, is_complete')
424
+ // data is typed as { id: string; title: string; is_complete: boolean }[] | null
425
+
426
+ // Automate type generation in CI:
427
+ // Add to package.json scripts: "types:supabase": "supabase gen types typescript --linked > lib/database.types.ts"
428
+ ```
293
429
 
294
430
  ## Output
295
- - Anti-patterns identified
296
- - Fixes prioritized and implemented
297
- - Prevention measures in place
298
- - Code quality improved
431
+
432
+ - Security pitfalls identified: service_role exposure, missing RLS, permissive policies, no connection pooling
433
+ - Data integrity pitfalls fixed: `{ data, error }` handling, `.select()` after mutations, `.maybeSingle()` usage
434
+ - Performance pitfalls resolved: column-specific selects, JOIN queries, FK indexes
435
+ - Maintainability improved: singleton client, generated types
436
+ - Detection commands for automated scanning of each pitfall
299
437
 
300
438
  ## Error Handling
439
+
301
440
  | Issue | Cause | Solution |
302
441
  |-------|-------|----------|
303
- | Too many findings | Legacy codebase | Prioritize security first |
304
- | Pattern not detected | Complex code | Manual review |
305
- | False positive | Similar code | Whitelist exceptions |
306
- | Fix breaks tests | Behavior change | Update tests |
442
+ | `PGRST116: JSON object requested, multiple (or no) rows returned` | Used `.single()` when 0 or 2+ rows match | Use `.maybeSingle()` for optional lookups |
443
+ | `data` is `null` after insert | Missing `.select()` chain | Add `.select('column1, column2')` after `.insert()` |
444
+ | `TypeError: Cannot read property of null` | Destructured only `data`, ignoring `error` | Always destructure `{ data, error }` and check error first |
445
+ | `too many connections for role` | Direct connection from serverless | Use pooled connection string (port 6543) |
446
+ | `permission denied for table` | RLS blocking access, no matching policy | Check RLS policies match the authenticated user's JWT claims |
447
+ | `relation does not exist` | Table name typo, not caught at compile time | Use generated types for compile-time validation |
307
448
 
308
449
  ## Examples
309
450
 
310
- ### Quick Pitfall Scan
451
+ ### Quick Security Audit
452
+
311
453
  ```bash
312
- # Check for common pitfalls
313
- grep -r "sk_live_" --include="*.ts" src/ # Key leakage
314
- grep -r "console.log" --include="*.ts" src/ # Potential PII logging
454
+ # Check for all three critical security pitfalls in one pass
455
+ echo "=== Pitfall 1: Service role in client code ==="
456
+ grep -rn 'SERVICE_ROLE' --include="*.tsx" --include="*.ts" src/ app/ components/ 2>/dev/null || echo "Clean"
457
+
458
+ echo "=== Pitfall 2: Tables without RLS ==="
459
+ # Run in SQL Editor:
460
+ # SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND rowsecurity = false;
461
+
462
+ echo "=== Pitfall 3: Overly permissive policies ==="
463
+ # Run in SQL Editor:
464
+ # SELECT tablename, policyname FROM pg_policies WHERE qual = 'true' AND cmd != 'r';
465
+ ```
466
+
467
+ ### Code Review Checklist
468
+
469
+ ```markdown
470
+ ## Supabase PR Review Checklist
471
+
472
+ ### Security
473
+ - [ ] No `SERVICE_ROLE_KEY` in client-side code or `NEXT_PUBLIC_*` vars
474
+ - [ ] RLS enabled on all new tables
475
+ - [ ] RLS policies scope to `auth.uid()` or org membership (no `USING (true)` for writes)
476
+
477
+ ### Data Integrity
478
+ - [ ] All Supabase calls destructure `{ data, error }` and check error
479
+ - [ ] `.select()` chained after `.insert()`, `.update()`, `.upsert()`
480
+ - [ ] `.maybeSingle()` used for optional lookups (not `.single()`)
481
+
482
+ ### Performance
483
+ - [ ] Column names specified in `.select()` (no `select('*')`)
484
+ - [ ] No N+1 patterns (use embedded joins or `.in()`)
485
+ - [ ] Foreign key columns have indexes
486
+
487
+ ### Maintainability
488
+ - [ ] Single `createClient` instance (singleton pattern)
489
+ - [ ] Generated types used (not manual interfaces)
490
+ - [ ] Pooled connection string for serverless deployments
315
491
  ```
316
492
 
317
493
  ## Resources
318
- - [Supabase Security Guide](https://supabase.com/docs/security)
319
- - [Supabase Best Practices](https://supabase.com/docs/best-practices)
320
-
321
- ## Quick Reference Card
322
-
323
- | Pitfall | Detection | Prevention |
324
- |---------|-----------|------------|
325
- | Sync in request | High latency | Use queues |
326
- | Rate limit ignore | 429 errors | Implement backoff |
327
- | Key leakage | Git history scan | Env vars, .gitignore |
328
- | No idempotency | Duplicate records | Idempotency keys |
329
- | Unverified webhooks | Security audit | Signature verification |
330
- | Missing error handling | Crashes | Try-catch, types |
331
- | Hardcoded config | Code review | Environment variables |
332
- | No circuit breaker | Cascading failures | opossum, resilience4j |
333
- | Logging PII | Log audit | Redaction middleware |
334
- | No degradation | Total outages | Fallback systems |
494
+
495
+ - [Supabase Auth: Securing Your Data](https://supabase.com/docs/guides/auth#securing-your-data)
496
+ - [Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
497
+ - [TypeScript Support](https://supabase.com/docs/reference/javascript/typescript-support)
498
+ - [Connection Pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler)
499
+ - [Supabase JavaScript Client Reference](https://supabase.com/docs/reference/javascript/select)
500
+ - [PostgREST Error Codes](https://postgrest.org/en/stable/references/errors.html)
501
+
502
+ ## Next Steps
503
+
504
+ This completes the Supabase pitfalls reference. To start a new project with best practices from day one, see `supabase-hello-world`.