@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.
- package/LICENSE +1 -1
- package/README.md +73 -47
- package/package.json +4 -4
- package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
- package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
- package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
- package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
- package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
- package/skills/supabase-architecture-variants/SKILL.md +395 -216
- package/skills/supabase-architecture-variants/references/errors.md +11 -0
- package/skills/supabase-architecture-variants/references/examples.md +12 -0
- package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
- package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
- package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
- package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
- package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
- package/skills/supabase-ci-integration/SKILL.md +315 -67
- package/skills/supabase-ci-integration/references/errors.md +10 -0
- package/skills/supabase-ci-integration/references/examples.md +36 -0
- package/skills/supabase-ci-integration/references/implementation.md +54 -0
- package/skills/supabase-common-errors/SKILL.md +320 -62
- package/skills/supabase-common-errors/references/errors.md +53 -0
- package/skills/supabase-common-errors/references/examples.md +23 -0
- package/skills/supabase-cost-tuning/SKILL.md +365 -131
- package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
- package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
- package/skills/supabase-cost-tuning/references/errors.md +11 -0
- package/skills/supabase-cost-tuning/references/examples.md +15 -0
- package/skills/supabase-data-handling/SKILL.md +378 -145
- package/skills/supabase-data-handling/references/errors.md +11 -0
- package/skills/supabase-data-handling/references/examples.md +27 -0
- package/skills/supabase-data-handling/references/implementation.md +223 -0
- package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
- package/skills/supabase-debug-bundle/SKILL.md +267 -73
- package/skills/supabase-debug-bundle/references/errors.md +12 -0
- package/skills/supabase-debug-bundle/references/examples.md +24 -0
- package/skills/supabase-debug-bundle/references/implementation.md +54 -0
- package/skills/supabase-deploy-integration/SKILL.md +258 -147
- package/skills/supabase-deploy-integration/references/errors.md +11 -0
- package/skills/supabase-deploy-integration/references/examples.md +21 -0
- package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
- package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
- package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
- package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
- package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
- package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
- package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
- package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
- package/skills/supabase-hello-world/SKILL.md +160 -54
- package/skills/supabase-incident-runbook/SKILL.md +453 -131
- package/skills/supabase-incident-runbook/references/errors.md +11 -0
- package/skills/supabase-incident-runbook/references/examples.md +10 -0
- package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
- package/skills/supabase-install-auth/SKILL.md +186 -50
- package/skills/supabase-install-auth/references/examples.md +102 -0
- package/skills/supabase-known-pitfalls/SKILL.md +411 -241
- package/skills/supabase-known-pitfalls/references/errors.md +11 -0
- package/skills/supabase-known-pitfalls/references/examples.md +12 -0
- package/skills/supabase-load-scale/SKILL.md +346 -217
- package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
- package/skills/supabase-load-scale/references/errors.md +11 -0
- package/skills/supabase-load-scale/references/examples.md +26 -0
- package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
- package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
- package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
- package/skills/supabase-local-dev-loop/SKILL.md +272 -73
- package/skills/supabase-local-dev-loop/references/errors.md +11 -0
- package/skills/supabase-local-dev-loop/references/examples.md +21 -0
- package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
- package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
- package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
- package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
- package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
- package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
- package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
- package/skills/supabase-multi-env-setup/SKILL.md +393 -152
- package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
- package/skills/supabase-multi-env-setup/references/errors.md +11 -0
- package/skills/supabase-multi-env-setup/references/examples.md +11 -0
- package/skills/supabase-observability/SKILL.md +318 -196
- package/skills/supabase-observability/references/alert-configuration.md +40 -0
- package/skills/supabase-observability/references/errors.md +11 -0
- package/skills/supabase-observability/references/examples.md +13 -0
- package/skills/supabase-observability/references/metrics-collection.md +65 -0
- package/skills/supabase-performance-tuning/SKILL.md +304 -160
- package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
- package/skills/supabase-performance-tuning/references/errors.md +11 -0
- package/skills/supabase-performance-tuning/references/examples.md +13 -0
- package/skills/supabase-policy-guardrails/SKILL.md +248 -221
- package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
- package/skills/supabase-policy-guardrails/references/errors.md +11 -0
- package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
- package/skills/supabase-policy-guardrails/references/examples.md +10 -0
- package/skills/supabase-prod-checklist/SKILL.md +474 -84
- package/skills/supabase-prod-checklist/references/errors.md +63 -0
- package/skills/supabase-prod-checklist/references/examples.md +153 -0
- package/skills/supabase-prod-checklist/references/implementation.md +113 -0
- package/skills/supabase-rate-limits/SKILL.md +311 -98
- package/skills/supabase-rate-limits/references/errors.md +11 -0
- package/skills/supabase-rate-limits/references/examples.md +46 -0
- package/skills/supabase-rate-limits/references/implementation.md +66 -0
- package/skills/supabase-reference-architecture/SKILL.md +249 -182
- package/skills/supabase-reference-architecture/references/errors.md +29 -0
- package/skills/supabase-reference-architecture/references/examples.md +116 -0
- package/skills/supabase-reference-architecture/references/key-components.md +244 -0
- package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
- package/skills/supabase-reliability-patterns/SKILL.md +229 -234
- package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
- package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
- package/skills/supabase-reliability-patterns/references/errors.md +11 -0
- package/skills/supabase-reliability-patterns/references/examples.md +11 -0
- package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
- package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
- package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
- package/skills/supabase-sdk-patterns/SKILL.md +388 -99
- package/skills/supabase-sdk-patterns/references/errors.md +11 -0
- package/skills/supabase-sdk-patterns/references/examples.md +45 -0
- package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
- package/skills/supabase-security-basics/SKILL.md +282 -102
- package/skills/supabase-security-basics/references/errors.md +10 -0
- package/skills/supabase-security-basics/references/examples.md +70 -0
- package/skills/supabase-security-basics/references/implementation.md +39 -0
- package/skills/supabase-upgrade-migration/SKILL.md +248 -66
- package/skills/supabase-upgrade-migration/references/errors.md +10 -0
- package/skills/supabase-upgrade-migration/references/examples.md +51 -0
- package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
- package/skills/supabase-webhooks-events/SKILL.md +412 -138
- package/skills/supabase-webhooks-events/references/errors.md +55 -0
- package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
- package/skills/supabase-webhooks-events/references/examples.md +133 -0
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
### Pitfall 1: Exposing service_role Key in Client Code
|
|
56
56
|
|
|
57
|
-
### ❌ Anti-Pattern
|
|
58
57
|
```typescript
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
### Pitfall 5: Not Handling { data, error }
|
|
162
173
|
|
|
163
|
-
### ❌ Anti-Pattern
|
|
164
174
|
```typescript
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
###
|
|
215
|
+
### Pitfall 7: .single() on Empty or Multiple Results
|
|
216
|
+
|
|
200
217
|
```typescript
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
250
|
+
### Pitfall 8: select('*') Everywhere
|
|
210
251
|
|
|
211
|
-
### ❌ Anti-Pattern
|
|
212
252
|
```typescript
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
###
|
|
220
|
-
```typescript
|
|
221
|
-
import CircuitBreaker from 'opossum';
|
|
271
|
+
### Pitfall 9: N+1 Query Pattern
|
|
222
272
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
for (const
|
|
231
|
-
await
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
###
|
|
390
|
+
### Pitfall 12: Not Using Generated Types
|
|
391
|
+
|
|
267
392
|
```typescript
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
414
|
+
type TodoInsert = Database['public']['Tables']['todos']['Insert']
|
|
415
|
+
// { title: string, user_id?: string, is_complete?: boolean }
|
|
284
416
|
|
|
285
|
-
|
|
286
|
-
|
|
417
|
+
// Step 3: Pass the Database type to createClient
|
|
418
|
+
import { createClient } from '@supabase/supabase-js'
|
|
287
419
|
|
|
288
|
-
|
|
289
|
-
Replace anti-patterns with recommended patterns.
|
|
420
|
+
const supabase = createClient<Database>(url, key)
|
|
290
421
|
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
296
|
-
-
|
|
297
|
-
-
|
|
298
|
-
-
|
|
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
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
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
|
|
451
|
+
### Quick Security Audit
|
|
452
|
+
|
|
311
453
|
```bash
|
|
312
|
-
# Check for
|
|
313
|
-
|
|
314
|
-
grep -
|
|
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
|
-
|
|
319
|
-
- [Supabase
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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`.
|