@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
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
## Migration Review, Cost Alerts, and Security Audit
|
|
2
|
+
|
|
3
|
+
### GitHub Actions Migration Guardrails
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
# .github/workflows/supabase-guardrails.yml
|
|
7
|
+
name: Supabase Migration Guardrails
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
paths:
|
|
12
|
+
- 'supabase/migrations/**'
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
migration-review:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: supabase/setup-cli@v1
|
|
20
|
+
|
|
21
|
+
- name: Start local Supabase
|
|
22
|
+
run: supabase start
|
|
23
|
+
|
|
24
|
+
- name: Apply migrations
|
|
25
|
+
run: supabase db reset
|
|
26
|
+
|
|
27
|
+
- name: Check RLS enabled on all public tables
|
|
28
|
+
run: |
|
|
29
|
+
MISSING_RLS=$(supabase db query "
|
|
30
|
+
SELECT tablename FROM pg_tables
|
|
31
|
+
WHERE schemaname = 'public'
|
|
32
|
+
AND rowsecurity = false
|
|
33
|
+
AND tablename NOT LIKE '\_%'
|
|
34
|
+
AND tablename NOT IN ('schema_migrations')
|
|
35
|
+
" --output csv | tail -n +2)
|
|
36
|
+
|
|
37
|
+
if [ -n "$MISSING_RLS" ]; then
|
|
38
|
+
echo "::error::Tables missing RLS: $MISSING_RLS"
|
|
39
|
+
echo "Fix: ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY;"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
echo "All public tables have RLS enabled"
|
|
43
|
+
|
|
44
|
+
- name: Check migration naming convention
|
|
45
|
+
run: |
|
|
46
|
+
for file in supabase/migrations/*.sql; do
|
|
47
|
+
basename=$(basename "$file")
|
|
48
|
+
if ! echo "$basename" | grep -qE '^[0-9]{14}_(create|alter|drop|add|remove|update|fix|seed|enable|disable)_[a-z_]+\.sql$'; then
|
|
49
|
+
echo "::error::Migration '$basename' violates naming convention"
|
|
50
|
+
echo "Expected: <14-digit-timestamp>_<verb>_<description>.sql"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
echo "Migration naming convention check passed"
|
|
55
|
+
|
|
56
|
+
- name: Block unannotated destructive operations
|
|
57
|
+
run: |
|
|
58
|
+
for file in supabase/migrations/*.sql; do
|
|
59
|
+
if grep -qiE 'DROP TABLE|DROP COLUMN|TRUNCATE|DELETE FROM.*WHERE\s+(1=1|true)' "$file"; then
|
|
60
|
+
if ! grep -qi '-- APPROVED-DESTRUCTIVE:' "$file"; then
|
|
61
|
+
echo "::error::Destructive operation in $file without approval annotation"
|
|
62
|
+
echo "Add '-- APPROVED-DESTRUCTIVE: <reason>' to acknowledge"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
echo "Destructive operation check passed"
|
|
68
|
+
|
|
69
|
+
- name: Validate naming conventions
|
|
70
|
+
run: |
|
|
71
|
+
ISSUES=$(supabase db query "SELECT * FROM public.validate_naming_conventions()" --output csv | tail -n +2)
|
|
72
|
+
if [ -n "$ISSUES" ]; then
|
|
73
|
+
echo "::warning::Naming convention issues found:"
|
|
74
|
+
echo "$ISSUES"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
- name: Check foreign key indexes
|
|
78
|
+
run: |
|
|
79
|
+
MISSING_INDEXES=$(supabase db query "
|
|
80
|
+
SELECT
|
|
81
|
+
tc.table_name,
|
|
82
|
+
kcu.column_name
|
|
83
|
+
FROM information_schema.table_constraints tc
|
|
84
|
+
JOIN information_schema.key_column_usage kcu
|
|
85
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
86
|
+
LEFT JOIN pg_indexes pi
|
|
87
|
+
ON pi.tablename = tc.table_name
|
|
88
|
+
AND pi.indexdef LIKE '%' || kcu.column_name || '%'
|
|
89
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
90
|
+
AND tc.table_schema = 'public'
|
|
91
|
+
AND pi.indexname IS NULL
|
|
92
|
+
" --output csv | tail -n +2)
|
|
93
|
+
|
|
94
|
+
if [ -n "$MISSING_INDEXES" ]; then
|
|
95
|
+
echo "::warning::Foreign key columns missing indexes: $MISSING_INDEXES"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
- name: Stop Supabase
|
|
99
|
+
if: always()
|
|
100
|
+
run: supabase stop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pre-Commit Hook for Secrets and SQL Lint
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
#!/bin/bash
|
|
107
|
+
# scripts/supabase-pre-commit.sh
|
|
108
|
+
set -euo pipefail
|
|
109
|
+
|
|
110
|
+
echo "Running Supabase pre-commit checks..."
|
|
111
|
+
|
|
112
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
113
|
+
|
|
114
|
+
# Check 1: No hardcoded Supabase keys (JWT format)
|
|
115
|
+
if echo "$STAGED_FILES" | grep -v '.env' | grep -v 'pnpm-lock' | \
|
|
116
|
+
xargs grep -lE 'eyJ[A-Za-z0-9_-]{50,}\.' 2>/dev/null; then
|
|
117
|
+
echo "ERROR: Possible Supabase API key in staged files"
|
|
118
|
+
echo "Use environment variables instead"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Check 2: No connection strings
|
|
123
|
+
if echo "$STAGED_FILES" | xargs grep -lE 'postgres://postgres\.[a-z]+:' 2>/dev/null; then
|
|
124
|
+
echo "ERROR: Supabase connection string in staged files"
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Check 3: Migration files have RLS (new tables)
|
|
129
|
+
for file in $(echo "$STAGED_FILES" | grep 'supabase/migrations/.*\.sql$' || true); do
|
|
130
|
+
if grep -qi 'CREATE TABLE public\.' "$file"; then
|
|
131
|
+
if ! grep -qi 'ENABLE ROW LEVEL SECURITY' "$file"; then
|
|
132
|
+
echo "ERROR: $file creates a table without enabling RLS"
|
|
133
|
+
echo "Add: ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY;"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
echo "Supabase pre-commit checks passed"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Install with Husky
|
|
144
|
+
npx husky add .husky/pre-commit 'bash scripts/supabase-pre-commit.sh'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Step 3 — Cost Alerts and Security Audit Scripts
|
|
148
|
+
|
|
149
|
+
### Cost Alert Configuration
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// scripts/supabase-cost-monitor.ts
|
|
153
|
+
import { createClient } from '@supabase/supabase-js'
|
|
154
|
+
|
|
155
|
+
// Use the Supabase Management API for cost monitoring
|
|
156
|
+
const SUPABASE_ACCESS_TOKEN = process.env.SUPABASE_ACCESS_TOKEN!
|
|
157
|
+
const PROJECT_REF = process.env.SUPABASE_PROJECT_REF!
|
|
158
|
+
|
|
159
|
+
interface UsageMetrics {
|
|
160
|
+
database_size_gb: number
|
|
161
|
+
storage_size_gb: number
|
|
162
|
+
bandwidth_gb: number
|
|
163
|
+
edge_function_invocations: number
|
|
164
|
+
monthly_active_users: number
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Cost thresholds — adjust per your budget
|
|
168
|
+
const THRESHOLDS = {
|
|
169
|
+
database_size_gb: 8, // Pro includes 8 GB
|
|
170
|
+
storage_size_gb: 100, // Pro includes 100 GB
|
|
171
|
+
bandwidth_gb: 250, // Pro includes 250 GB
|
|
172
|
+
edge_function_invocations: 2_000_000, // Pro includes 2M
|
|
173
|
+
monthly_active_users: 100_000, // Pro limit
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function checkCostAlerts() {
|
|
177
|
+
// Fetch current usage via Supabase Management API
|
|
178
|
+
const response = await fetch(
|
|
179
|
+
`https://api.supabase.com/v1/projects/${PROJECT_REF}/usage`,
|
|
180
|
+
{
|
|
181
|
+
headers: { Authorization: `Bearer ${SUPABASE_ACCESS_TOKEN}` },
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
console.error('Failed to fetch usage:', response.statusText)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const usage: UsageMetrics = await response.json()
|
|
191
|
+
|
|
192
|
+
const alerts: string[] = []
|
|
193
|
+
|
|
194
|
+
for (const [metric, threshold] of Object.entries(THRESHOLDS)) {
|
|
195
|
+
const current = usage[metric as keyof UsageMetrics] as number
|
|
196
|
+
const percent = (current / threshold) * 100
|
|
197
|
+
|
|
198
|
+
if (percent >= 90) {
|
|
199
|
+
alerts.push(`CRITICAL: ${metric} at ${percent.toFixed(1)}% (${current}/${threshold})`)
|
|
200
|
+
} else if (percent >= 75) {
|
|
201
|
+
alerts.push(`WARNING: ${metric} at ${percent.toFixed(1)}% (${current}/${threshold})`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (alerts.length > 0) {
|
|
206
|
+
console.warn('Cost alerts:\n' + alerts.join('\n'))
|
|
207
|
+
// Send to Slack, PagerDuty, email, etc.
|
|
208
|
+
await sendCostAlert(alerts)
|
|
209
|
+
} else {
|
|
210
|
+
console.log('All usage metrics within budget')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function sendCostAlert(alerts: string[]) {
|
|
215
|
+
// Example: Slack webhook
|
|
216
|
+
const webhookUrl = process.env.SLACK_WEBHOOK_URL
|
|
217
|
+
if (!webhookUrl) return
|
|
218
|
+
|
|
219
|
+
await fetch(webhookUrl, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
text: `*Supabase Cost Alert* (${PROJECT_REF})\n${alerts.join('\n')}`,
|
|
224
|
+
}),
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Security Audit Script
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// scripts/supabase-security-audit.ts
|
|
233
|
+
import { createClient } from '@supabase/supabase-js'
|
|
234
|
+
|
|
235
|
+
const supabase = createClient(
|
|
236
|
+
process.env.SUPABASE_URL!,
|
|
237
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
238
|
+
{ auth: { autoRefreshToken: false, persistSession: false } }
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
interface AuditFinding {
|
|
242
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
243
|
+
category: string
|
|
244
|
+
description: string
|
|
245
|
+
remediation: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function runSecurityAudit(): Promise<AuditFinding[]> {
|
|
249
|
+
const findings: AuditFinding[] = []
|
|
250
|
+
|
|
251
|
+
// Check 1: Tables without RLS
|
|
252
|
+
const { data: noRls } = await supabase.rpc('run_sql', {
|
|
253
|
+
sql: `
|
|
254
|
+
SELECT tablename FROM pg_tables
|
|
255
|
+
WHERE schemaname = 'public'
|
|
256
|
+
AND rowsecurity = false
|
|
257
|
+
AND tablename NOT LIKE '\\_%'
|
|
258
|
+
`,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
for (const row of noRls ?? []) {
|
|
262
|
+
findings.push({
|
|
263
|
+
severity: 'critical',
|
|
264
|
+
category: 'RLS',
|
|
265
|
+
description: `Table "${row.tablename}" has RLS disabled`,
|
|
266
|
+
remediation: `ALTER TABLE public.${row.tablename} ENABLE ROW LEVEL SECURITY;`,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check 2: Tables with RLS enabled but no policies
|
|
271
|
+
const { data: noPolicies } = await supabase.rpc('run_sql', {
|
|
272
|
+
sql: `
|
|
273
|
+
SELECT t.tablename
|
|
274
|
+
FROM pg_tables t
|
|
275
|
+
LEFT JOIN pg_policies p ON p.tablename = t.tablename AND p.schemaname = t.schemaname
|
|
276
|
+
WHERE t.schemaname = 'public'
|
|
277
|
+
AND t.rowsecurity = true
|
|
278
|
+
AND p.policyname IS NULL
|
|
279
|
+
`,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
for (const row of noPolicies ?? []) {
|
|
283
|
+
findings.push({
|
|
284
|
+
severity: 'high',
|
|
285
|
+
category: 'RLS',
|
|
286
|
+
description: `Table "${row.tablename}" has RLS enabled but no policies (blocks all access)`,
|
|
287
|
+
remediation: 'Add appropriate RLS policies or this table is inaccessible via API',
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check 3: Overly permissive policies (USING (true) for non-public tables)
|
|
292
|
+
const { data: permissive } = await supabase.rpc('run_sql', {
|
|
293
|
+
sql: `
|
|
294
|
+
SELECT tablename, policyname, qual
|
|
295
|
+
FROM pg_policies
|
|
296
|
+
WHERE schemaname = 'public'
|
|
297
|
+
AND qual = 'true'
|
|
298
|
+
AND cmd != 'SELECT'
|
|
299
|
+
`,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
for (const row of permissive ?? []) {
|
|
303
|
+
findings.push({
|
|
304
|
+
severity: 'high',
|
|
305
|
+
category: 'RLS',
|
|
306
|
+
description: `Policy "${row.policyname}" on "${row.tablename}" allows unrestricted writes (USING true)`,
|
|
307
|
+
remediation: 'Restrict policy to owner or organization scope',
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check 4: Foreign key columns without indexes
|
|
312
|
+
const { data: missingIdx } = await supabase.rpc('run_sql', {
|
|
313
|
+
sql: `
|
|
314
|
+
SELECT
|
|
315
|
+
tc.table_name,
|
|
316
|
+
kcu.column_name
|
|
317
|
+
FROM information_schema.table_constraints tc
|
|
318
|
+
JOIN information_schema.key_column_usage kcu
|
|
319
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
320
|
+
LEFT JOIN pg_indexes pi
|
|
321
|
+
ON pi.tablename = tc.table_name
|
|
322
|
+
AND pi.indexdef LIKE '%' || kcu.column_name || '%'
|
|
323
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
324
|
+
AND tc.table_schema = 'public'
|
|
325
|
+
AND pi.indexname IS NULL
|
|
326
|
+
`,
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
for (const row of missingIdx ?? []) {
|
|
330
|
+
findings.push({
|
|
331
|
+
severity: 'medium',
|
|
332
|
+
category: 'Performance',
|
|
333
|
+
description: `Foreign key ${row.table_name}.${row.column_name} has no index`,
|
|
334
|
+
remediation: `CREATE INDEX idx_${row.table_name}_${row.column_name} ON public.${row.table_name}(${row.column_name});`,
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check 5: Storage buckets without RLS
|
|
339
|
+
const { data: buckets } = await supabase.storage.listBuckets()
|
|
340
|
+
for (const bucket of buckets ?? []) {
|
|
341
|
+
if (bucket.public) {
|
|
342
|
+
findings.push({
|
|
343
|
+
severity: 'low',
|
|
344
|
+
category: 'Storage',
|
|
345
|
+
description: `Bucket "${bucket.name}" is public — verify this is intentional`,
|
|
346
|
+
remediation: 'Set bucket to private if it contains sensitive files',
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return findings
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Run and display results
|
|
355
|
+
async function main() {
|
|
356
|
+
const findings = await runSecurityAudit()
|
|
357
|
+
|
|
358
|
+
const critical = findings.filter(f => f.severity === 'critical')
|
|
359
|
+
const high = findings.filter(f => f.severity === 'high')
|
|
360
|
+
|
|
361
|
+
console.log(`\nSecurity Audit Results:`)
|
|
362
|
+
console.log(` Critical: ${critical.length}`)
|
|
363
|
+
console.log(` High: ${high.length}`)
|
|
364
|
+
console.log(` Medium: ${findings.filter(f => f.severity === 'medium').length}`)
|
|
365
|
+
console.log(` Low: ${findings.filter(f => f.severity === 'low').length}`)
|
|
366
|
+
|
|
367
|
+
for (const finding of findings) {
|
|
368
|
+
console.log(`\n[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.description}`)
|
|
369
|
+
console.log(` Fix: ${finding.remediation}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Exit with error code if critical/high issues found
|
|
373
|
+
if (critical.length > 0 || high.length > 0) {
|
|
374
|
+
process.exit(1)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
main()
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Scheduled Audit via Edge Function
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// supabase/functions/security-audit/index.ts
|
|
385
|
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
|
386
|
+
|
|
387
|
+
Deno.serve(async () => {
|
|
388
|
+
const supabase = createClient(
|
|
389
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
390
|
+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
// Check tables without RLS
|
|
394
|
+
const { data: noRls } = await supabase
|
|
395
|
+
.from('pg_tables')
|
|
396
|
+
.select('tablename')
|
|
397
|
+
.eq('schemaname', 'public')
|
|
398
|
+
.eq('rowsecurity', false)
|
|
399
|
+
|
|
400
|
+
const issues = (noRls ?? []).map(t => t.tablename)
|
|
401
|
+
|
|
402
|
+
if (issues.length > 0) {
|
|
403
|
+
// Store audit result
|
|
404
|
+
await supabase.from('audit_log').insert({
|
|
405
|
+
event: 'security_audit',
|
|
406
|
+
severity: 'critical',
|
|
407
|
+
details: { tables_without_rls: issues },
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return new Response(JSON.stringify({
|
|
412
|
+
status: issues.length === 0 ? 'pass' : 'fail',
|
|
413
|
+
tables_without_rls: issues,
|
|
414
|
+
checked_at: new Date().toISOString(),
|
|
415
|
+
}))
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Output
|
|
420
|
+
|
|
421
|
+
- Shared RLS policy library with owner-only, org-scoped, and public-read templates
|
|
422
|
+
- Naming convention validation function checking tables, columns, FKs, and booleans
|
|
423
|
+
- CI pipeline enforcing RLS, naming, and destructive operation controls
|
|
424
|
+
- Pre-commit hook blocking hardcoded secrets and tables without RLS
|
|
425
|
+
- Cost monitoring script with configurable thresholds and Slack alerting
|
|
426
|
+
- Security audit script detecting missing RLS, permissive policies, and missing indexes
|
|
427
|
+
- Scheduled Edge Function for continuous security monitoring
|
|
428
|
+
|
|
429
|
+
## Error Handling
|
|
430
|
+
|
|
431
|
+
| Issue | Cause | Solution |
|
|
432
|
+
|-------|-------|----------|
|
|
433
|
+
| CI RLS check fails on new table | Migration missing `ENABLE ROW LEVEL SECURITY` | Add `ALTER TABLE` after `CREATE TABLE` in same migration |
|
|
434
|
+
| Naming convention false positive | Table is intentionally singular (e.g., `config`) | Add to exclusion list in validation function |
|
|
435
|
+
| Cost alert not firing | Missing `SUPABASE_ACCESS_TOKEN` | Generate token at supabase.com/dashboard/account/tokens |
|
|
436
|
+
| Security audit times out | Too many tables to scan | Run audit on specific schemas or paginate results |
|
|
437
|
+
| Pre-commit blocks legitimate JWT in test | Test fixture contains JWT-like string | Add test file path to exclusion pattern |
|
|
438
|
+
| RLS template function not found | Migration not applied | Run `supabase db reset` or apply migration manually |
|
|
439
|
+
|
|
440
|
+
## Examples
|
|
441
|
+
|
|
442
|
+
### Apply RLS Template to a New Table
|
|
443
|
+
|
|
444
|
+
```sql
|
|
445
|
+
-- Create the table
|
|
446
|
+
CREATE TABLE public.tasks (
|
|
447
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
448
|
+
org_id uuid NOT NULL REFERENCES public.organizations(id),
|
|
449
|
+
title text NOT NULL,
|
|
450
|
+
is_complete boolean DEFAULT false,
|
|
451
|
+
created_by uuid REFERENCES auth.users(id),
|
|
452
|
+
created_at timestamptz DEFAULT now()
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
-- Apply org-scoped RLS template (with delete for admins)
|
|
456
|
+
SELECT public.rls_org_scoped('tasks', 'org_id', true);
|
|
457
|
+
|
|
458
|
+
-- Create index on foreign key
|
|
459
|
+
CREATE INDEX idx_tasks_org_id ON public.tasks(org_id);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Run Security Audit Locally
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
npx tsx scripts/supabase-security-audit.ts
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Check Naming Conventions
|
|
469
|
+
|
|
470
|
+
```sql
|
|
471
|
+
SELECT * FROM public.validate_naming_conventions();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Resources
|
|
475
|
+
|
|
476
|
+
- [Supabase Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
|
477
|
+
- [Supabase CLI Migrations](https://supabase.com/docs/guides/cli/managing-environments)
|
|
478
|
+
- [Supabase Management API](https://supabase.com/docs/reference/api/introduction)
|
|
479
|
+
- [Supabase Pricing](https://supabase.com/pricing)
|
|
480
|
+
- [PostgreSQL Naming Conventions](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)
|
|
481
|
+
|
|
482
|
+
## Next Steps
|
|
483
|
+
|
|
484
|
+
For architecture patterns across different app types, see `supabase-architecture-variants`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Error Handling Reference
|
|
2
|
+
|
|
3
|
+
| Issue | Cause | Solution |
|
|
4
|
+
|-------|-------|----------|
|
|
5
|
+
| ESLint rule not firing | Wrong config | Check plugin registration |
|
|
6
|
+
| Pre-commit skipped | --no-verify | Enforce in CI |
|
|
7
|
+
| Policy false positive | Regex too broad | Narrow pattern match |
|
|
8
|
+
| Guardrail triggered | Actual issue | Fix or whitelist |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Eslint Rules
|
|
2
|
+
|
|
3
|
+
## ESLint Rules
|
|
4
|
+
|
|
5
|
+
### Custom Supabase Plugin
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// eslint-plugin-supabase/rules/no-hardcoded-keys.js
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow hardcoded Supabase API keys',
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
Literal(node) {
|
|
20
|
+
if (typeof node.value === 'string') {
|
|
21
|
+
if (node.value.match(/^sk_(live|test)_[a-zA-Z0-9]{24,}/)) {
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: 'Hardcoded Supabase API key detected',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### ESLint Configuration
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// .eslintrc.js
|
|
38
|
+
module.exports = {
|
|
39
|
+
plugins: ['supabase'],
|
|
40
|
+
rules: {
|
|
41
|
+
'supabase/no-hardcoded-keys': 'error',
|
|
42
|
+
'supabase/require-error-handling': 'warn',
|
|
43
|
+
'supabase/use-typed-client': 'warn',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
## Examples
|
|
2
|
+
|
|
3
|
+
### Quick ESLint Check
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx eslint --plugin supabase --rule 'supabase/no-hardcoded-keys: error' src/
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
|