qualia-framework 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/cli.js +519 -0
- package/framework/agents/architecture-strategist.md +53 -0
- package/framework/agents/backend-agent.md +150 -0
- package/framework/agents/code-simplicity-reviewer.md +86 -0
- package/framework/agents/frontend-agent.md +111 -0
- package/framework/agents/kieran-typescript-reviewer.md +96 -0
- package/framework/agents/performance-oracle.md +111 -0
- package/framework/agents/qualia-codebase-mapper.md +760 -0
- package/framework/agents/qualia-debugger.md +1203 -0
- package/framework/agents/qualia-executor.md +881 -0
- package/framework/agents/qualia-integration-checker.md +423 -0
- package/framework/agents/qualia-phase-researcher.md +453 -0
- package/framework/agents/qualia-plan-checker.md +699 -0
- package/framework/agents/qualia-planner.md +1241 -0
- package/framework/agents/qualia-project-researcher.md +602 -0
- package/framework/agents/qualia-research-synthesizer.md +236 -0
- package/framework/agents/qualia-roadmapper.md +605 -0
- package/framework/agents/qualia-verifier.md +685 -0
- package/framework/agents/team-orchestrator.md +228 -0
- package/framework/agents/teams/full-stack-team.md +48 -0
- package/framework/agents/teams/optimize-team.md +53 -0
- package/framework/agents/teams/review-team.md +62 -0
- package/framework/agents/teams/ship-team.md +86 -0
- package/framework/agents/test-agent.md +182 -0
- package/framework/askpass.sh +2 -0
- package/framework/commands/design.md +53 -0
- package/framework/commands/quick-db.md +22 -0
- package/framework/config/retention.json +35 -0
- package/framework/core/PRINCIPLES.md +77 -0
- package/framework/hooks/auto-format.sh +45 -0
- package/framework/hooks/block-env-edit.sh +42 -0
- package/framework/hooks/branch-guard.sh +46 -0
- package/framework/hooks/confirm-delete.sh +56 -0
- package/framework/hooks/migration-validate.sh +68 -0
- package/framework/hooks/notification-speak.sh +15 -0
- package/framework/hooks/pre-commit.sh +80 -0
- package/framework/hooks/pre-compact.sh +55 -0
- package/framework/hooks/pre-deploy-gate.sh +151 -0
- package/framework/hooks/qualia-colors.sh +32 -0
- package/framework/hooks/retention-cleanup.sh +43 -0
- package/framework/hooks/save-session-state.sh +153 -0
- package/framework/hooks/session-context-loader.sh +28 -0
- package/framework/hooks/session-learn.sh +30 -0
- package/framework/knowledge/claudecode-bible.md +1384 -0
- package/framework/knowledge/client-prefs.md +22 -0
- package/framework/knowledge/common-fixes.md +25 -0
- package/framework/knowledge/deployment-map.md +35 -0
- package/framework/knowledge/email-signature.html +1 -0
- package/framework/knowledge/employees.md +8 -0
- package/framework/knowledge/learned-patterns.md +51 -0
- package/framework/knowledge/optimization-research-2026.md +137 -0
- package/framework/knowledge/qualia-context.md +67 -0
- package/framework/knowledge/supabase-patterns.md +50 -0
- package/framework/knowledge/voice-agent-patterns.md +46 -0
- package/framework/qualia-engine/VERSION +1 -0
- package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
- package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
- package/framework/qualia-engine/references/checkpoints.md +775 -0
- package/framework/qualia-engine/references/continuation-format.md +249 -0
- package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
- package/framework/qualia-engine/references/design-quality.md +56 -0
- package/framework/qualia-engine/references/git-integration.md +254 -0
- package/framework/qualia-engine/references/git-planning-commit.md +50 -0
- package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
- package/framework/qualia-engine/references/model-profiles.md +73 -0
- package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
- package/framework/qualia-engine/references/planning-config.md +195 -0
- package/framework/qualia-engine/references/questioning.md +141 -0
- package/framework/qualia-engine/references/tdd.md +263 -0
- package/framework/qualia-engine/references/ui-brand.md +160 -0
- package/framework/qualia-engine/references/verification-patterns.md +612 -0
- package/framework/qualia-engine/templates/DEBUG.md +159 -0
- package/framework/qualia-engine/templates/DESIGN.md +81 -0
- package/framework/qualia-engine/templates/UAT.md +247 -0
- package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
- package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
- package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
- package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
- package/framework/qualia-engine/templates/codebase/stack.md +186 -0
- package/framework/qualia-engine/templates/codebase/structure.md +285 -0
- package/framework/qualia-engine/templates/codebase/testing.md +480 -0
- package/framework/qualia-engine/templates/config.json +35 -0
- package/framework/qualia-engine/templates/context.md +283 -0
- package/framework/qualia-engine/templates/continue-here.md +78 -0
- package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
- package/framework/qualia-engine/templates/discovery.md +146 -0
- package/framework/qualia-engine/templates/milestone-archive.md +123 -0
- package/framework/qualia-engine/templates/milestone.md +115 -0
- package/framework/qualia-engine/templates/phase-prompt.md +567 -0
- package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
- package/framework/qualia-engine/templates/project.md +184 -0
- package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
- package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
- package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
- package/framework/qualia-engine/templates/projects/website.md +137 -0
- package/framework/qualia-engine/templates/requirements.md +231 -0
- package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
- package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
- package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
- package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
- package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
- package/framework/qualia-engine/templates/research.md +552 -0
- package/framework/qualia-engine/templates/roadmap.md +202 -0
- package/framework/qualia-engine/templates/state.md +176 -0
- package/framework/qualia-engine/templates/summary-complex.md +59 -0
- package/framework/qualia-engine/templates/summary-minimal.md +41 -0
- package/framework/qualia-engine/templates/summary-standard.md +48 -0
- package/framework/qualia-engine/templates/summary.md +246 -0
- package/framework/qualia-engine/templates/user-setup.md +311 -0
- package/framework/qualia-engine/templates/verification-report.md +322 -0
- package/framework/qualia-engine/workflows/add-phase.md +179 -0
- package/framework/qualia-engine/workflows/add-todo.md +157 -0
- package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
- package/framework/qualia-engine/workflows/check-todos.md +176 -0
- package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
- package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
- package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
- package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
- package/framework/qualia-engine/workflows/execute-phase.md +559 -0
- package/framework/qualia-engine/workflows/execute-plan.md +438 -0
- package/framework/qualia-engine/workflows/help.md +470 -0
- package/framework/qualia-engine/workflows/insert-phase.md +220 -0
- package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
- package/framework/qualia-engine/workflows/map-codebase.md +327 -0
- package/framework/qualia-engine/workflows/new-milestone.md +363 -0
- package/framework/qualia-engine/workflows/new-project.md +1037 -0
- package/framework/qualia-engine/workflows/pause-work.md +122 -0
- package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
- package/framework/qualia-engine/workflows/plan-phase.md +422 -0
- package/framework/qualia-engine/workflows/progress.md +354 -0
- package/framework/qualia-engine/workflows/quick.md +252 -0
- package/framework/qualia-engine/workflows/remove-phase.md +326 -0
- package/framework/qualia-engine/workflows/research-phase.md +74 -0
- package/framework/qualia-engine/workflows/resume-project.md +306 -0
- package/framework/qualia-engine/workflows/set-profile.md +80 -0
- package/framework/qualia-engine/workflows/settings.md +145 -0
- package/framework/qualia-engine/workflows/transition.md +556 -0
- package/framework/qualia-engine/workflows/update.md +197 -0
- package/framework/qualia-engine/workflows/verify-phase.md +195 -0
- package/framework/qualia-engine/workflows/verify-work.md +625 -0
- package/framework/rules/context7.md +11 -0
- package/framework/rules/deployment.md +29 -0
- package/framework/rules/frontend.md +33 -0
- package/framework/rules/security.md +12 -0
- package/framework/rules/speed.md +20 -0
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +120 -0
- package/framework/scripts/bootstrap-pop-os.sh +354 -0
- package/framework/scripts/claude-voice +13 -0
- package/framework/scripts/cleanup.sh +131 -0
- package/framework/scripts/cowork-mode.sh +141 -0
- package/framework/scripts/generate-project-claude-md.sh +153 -0
- package/framework/scripts/load-test-webhook.js +172 -0
- package/framework/scripts/say.py +236 -0
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
- package/framework/scripts/speak.py +55 -0
- package/framework/scripts/speak.sh +18 -0
- package/framework/scripts/status.sh +138 -0
- package/framework/scripts/sync-to-framework.sh +65 -0
- package/framework/scripts/voice-hotkey.py +227 -0
- package/framework/scripts/voice-input.sh +51 -0
- package/framework/skills/animate/SKILL.md +202 -0
- package/framework/skills/bolder/SKILL.md +144 -0
- package/framework/skills/browser-qa/SKILL.md +536 -0
- package/framework/skills/clarify/SKILL.md +179 -0
- package/framework/skills/colorize/SKILL.md +170 -0
- package/framework/skills/critique/SKILL.md +126 -0
- package/framework/skills/deep-research/SKILL.md +271 -0
- package/framework/skills/delight/SKILL.md +329 -0
- package/framework/skills/deploy/SKILL.md +261 -0
- package/framework/skills/deploy-verify/SKILL.md +377 -0
- package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
- package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
- package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
- package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
- package/framework/skills/design-quieter/SKILL.md +130 -0
- package/framework/skills/distill/SKILL.md +149 -0
- package/framework/skills/docs-lookup/SKILL.md +78 -0
- package/framework/skills/fcm-notifications/SKILL.md +125 -0
- package/framework/skills/financial-ledger/SKILL.md +1039 -0
- package/framework/skills/frontend-master/NOTICE.md +4 -0
- package/framework/skills/frontend-master/SKILL.md +127 -0
- package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
- package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
- package/framework/skills/frontend-master/reference/motion-design.md +99 -0
- package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
- package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
- package/framework/skills/frontend-master/reference/typography.md +131 -0
- package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
- package/framework/skills/harden/SKILL.md +357 -0
- package/framework/skills/i18n-rtl/SKILL.md +752 -0
- package/framework/skills/learn/SKILL.md +71 -0
- package/framework/skills/memory/SKILL.md +50 -0
- package/framework/skills/mobile-expo/SKILL.md +864 -0
- package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
- package/framework/skills/nestjs-backend/README.md +73 -0
- package/framework/skills/nestjs-backend/SKILL.md +446 -0
- package/framework/skills/nestjs-backend/references/templates.md +1173 -0
- package/framework/skills/normalize/SKILL.md +79 -0
- package/framework/skills/onboard/SKILL.md +242 -0
- package/framework/skills/polish/SKILL.md +209 -0
- package/framework/skills/pr/SKILL.md +66 -0
- package/framework/skills/qualia/SKILL.md +153 -0
- package/framework/skills/qualia-add-todo/SKILL.md +68 -0
- package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
- package/framework/skills/qualia-check-todos/SKILL.md +55 -0
- package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
- package/framework/skills/qualia-debug/SKILL.md +149 -0
- package/framework/skills/qualia-design/SKILL.md +203 -0
- package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
- package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
- package/framework/skills/qualia-help/SKILL.md +67 -0
- package/framework/skills/qualia-idk/SKILL.md +352 -0
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
- package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
- package/framework/skills/qualia-new-project/SKILL.md +92 -0
- package/framework/skills/qualia-optimize/SKILL.md +417 -0
- package/framework/skills/qualia-pause-work/SKILL.md +96 -0
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
- package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
- package/framework/skills/qualia-progress/SKILL.md +53 -0
- package/framework/skills/qualia-quick/SKILL.md +89 -0
- package/framework/skills/qualia-research-phase/SKILL.md +88 -0
- package/framework/skills/qualia-resume-work/SKILL.md +62 -0
- package/framework/skills/qualia-review/SKILL.md +263 -0
- package/framework/skills/qualia-start/SKILL.md +182 -0
- package/framework/skills/qualia-verify-work/SKILL.md +105 -0
- package/framework/skills/qualia-workflow/SKILL.md +130 -0
- package/framework/skills/rag/SKILL.md +750 -0
- package/framework/skills/responsive/SKILL.md +231 -0
- package/framework/skills/retro/SKILL.md +284 -0
- package/framework/skills/sakani-conventions/SKILL.md +136 -0
- package/framework/skills/sakani-conventions/evals/evals.json +23 -0
- package/framework/skills/sakani-conventions/references/entities.md +365 -0
- package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
- package/framework/skills/seo-master/SKILL.md +490 -0
- package/framework/skills/seo-master/references/checklist.md +199 -0
- package/framework/skills/seo-master/references/structured-data.md +609 -0
- package/framework/skills/ship/SKILL.md +202 -0
- package/framework/skills/stack-researcher/SKILL.md +215 -0
- package/framework/skills/status/SKILL.md +154 -0
- package/framework/skills/status/scripts/health-check.sh +562 -0
- package/framework/skills/subscription-payments/SKILL.md +250 -0
- package/framework/skills/supabase/SKILL.md +973 -0
- package/framework/skills/supabase/references/templates.md +159 -0
- package/framework/skills/team/SKILL.md +67 -0
- package/framework/skills/test-runner/SKILL.md +202 -0
- package/framework/skills/voice-agent/SKILL.md +407 -0
- package/framework/skills/zoho-workflow/SKILL.md +51 -0
- package/framework/statusline-command.sh +117 -0
- package/package.json +24 -0
- package/profiles/fawzi.json +16 -0
- package/profiles/hasan.json +16 -0
- package/profiles/moayad.json +16 -0
- package/templates/CLAUDE-owner.md +52 -0
- package/templates/CLAUDE.md.hbs +58 -0
- package/templates/env.claude.template +12 -0
- package/templates/settings.json +141 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase
|
|
3
|
+
description: Complete Supabase operations via CLI + Management API. Full replacement for 32 MCP tools with zero context overhead. Schema, migrations, auth, RLS, edge functions, SQL execution, debugging, storage, branching.
|
|
4
|
+
tags: [supabase, database, auth, rls, edge-functions, postgres]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Supabase (CLI-First, MCP-Free)
|
|
8
|
+
|
|
9
|
+
Full `supabase` CLI + Management API access. Every MCP tool is covered.
|
|
10
|
+
|
|
11
|
+
## Project Discovery (ALWAYS DO FIRST)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Check linked project
|
|
15
|
+
supabase projects list # → MCP: list_projects
|
|
16
|
+
supabase status # → MCP: get_project (shows URL, keys, DB URL)
|
|
17
|
+
|
|
18
|
+
# 2. Link if needed
|
|
19
|
+
supabase link --project-ref <ref>
|
|
20
|
+
|
|
21
|
+
# 3. Get full schema (ALWAYS read before modifying)
|
|
22
|
+
supabase db dump --schema public # → MCP: list_tables
|
|
23
|
+
|
|
24
|
+
# 4. Generate types
|
|
25
|
+
supabase gen types typescript --linked > types/supabase.ts # → MCP: generate_typescript_types
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Complete MCP Tool → CLI/API Map (All 32 Tools)
|
|
29
|
+
|
|
30
|
+
### Account Tools (9)
|
|
31
|
+
| MCP Tool | Replacement |
|
|
32
|
+
|----------|-------------|
|
|
33
|
+
| `list_projects` | `supabase projects list` |
|
|
34
|
+
| `get_project` | `supabase status` or `supabase projects list` |
|
|
35
|
+
| `create_project` | `supabase projects create <name> --org-id <id> --db-password <pw> --region <region>` |
|
|
36
|
+
| `pause_project` | `curl -X POST "$SUPA_API/v1/projects/$REF/pause" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
37
|
+
| `restore_project` | `curl -X POST "$SUPA_API/v1/projects/$REF/restore" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
38
|
+
| `list_organizations` | `supabase orgs list` |
|
|
39
|
+
| `get_organization` | `supabase orgs list` |
|
|
40
|
+
| `get_cost` | `curl "$SUPA_API/v1/organizations/$ORG/billing/costs" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
41
|
+
| `confirm_cost` | Not needed — MCP-specific UX flow |
|
|
42
|
+
|
|
43
|
+
### Database Tools (5)
|
|
44
|
+
| MCP Tool | Replacement |
|
|
45
|
+
|----------|-------------|
|
|
46
|
+
| `list_tables` | `supabase db dump --schema public` or `supabase inspect db table-stats` |
|
|
47
|
+
| `list_extensions` | Execute SQL: `SELECT extname, extversion FROM pg_extension` |
|
|
48
|
+
| `list_migrations` | `supabase migration list` |
|
|
49
|
+
| `apply_migration` | `supabase migration new <name>` → edit SQL → `supabase db push` |
|
|
50
|
+
| `execute_sql` | **See "Execute SQL" section below** |
|
|
51
|
+
|
|
52
|
+
### Development Tools (3)
|
|
53
|
+
| MCP Tool | Replacement |
|
|
54
|
+
|----------|-------------|
|
|
55
|
+
| `get_project_url` | `supabase status` (shows API URL) |
|
|
56
|
+
| `get_publishable_keys` | `supabase projects api-keys` |
|
|
57
|
+
| `generate_typescript_types` | `supabase gen types typescript --linked` |
|
|
58
|
+
|
|
59
|
+
### Edge Functions Tools (3)
|
|
60
|
+
| MCP Tool | Replacement |
|
|
61
|
+
|----------|-------------|
|
|
62
|
+
| `list_edge_functions` | `supabase functions list` |
|
|
63
|
+
| `get_edge_function` | `supabase functions download <name>` |
|
|
64
|
+
| `deploy_edge_function` | `supabase functions deploy <name>` |
|
|
65
|
+
|
|
66
|
+
### Debugging Tools (2)
|
|
67
|
+
| MCP Tool | Replacement |
|
|
68
|
+
|----------|-------------|
|
|
69
|
+
| `get_logs` | `supabase functions logs <name> --tail` (functions) or Management API for other services |
|
|
70
|
+
| `get_advisors` | `supabase inspect db` (13 commands — BETTER than MCP advisors) |
|
|
71
|
+
|
|
72
|
+
### Branching Tools (6)
|
|
73
|
+
| MCP Tool | Replacement |
|
|
74
|
+
|----------|-------------|
|
|
75
|
+
| `create_branch` | `supabase branches create <name>` |
|
|
76
|
+
| `list_branches` | `supabase branches list` |
|
|
77
|
+
| `delete_branch` | `supabase branches delete <name>` |
|
|
78
|
+
| `merge_branch` | `curl -X POST "$SUPA_API/v1/branches/$BRANCH_ID/merge" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
79
|
+
| `reset_branch` | `curl -X POST "$SUPA_API/v1/branches/$BRANCH_ID/reset" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
80
|
+
| `rebase_branch` | `curl -X POST "$SUPA_API/v1/branches/$BRANCH_ID/rebase" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
81
|
+
|
|
82
|
+
### Storage Tools (3)
|
|
83
|
+
| MCP Tool | Replacement |
|
|
84
|
+
|----------|-------------|
|
|
85
|
+
| `list_storage_buckets` | Execute SQL: `SELECT * FROM storage.buckets` or `supabase storage ls` |
|
|
86
|
+
| `get_storage_config` | `curl "$SUPA_API/v1/projects/$REF/config/storage" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"` |
|
|
87
|
+
| `update_storage_config` | `curl -X PATCH "$SUPA_API/v1/projects/$REF/config/storage" -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" -d '{...}'` |
|
|
88
|
+
|
|
89
|
+
### Knowledge Base (1)
|
|
90
|
+
| MCP Tool | Replacement |
|
|
91
|
+
|----------|-------------|
|
|
92
|
+
| `search_docs` | WebSearch or `WebFetch: https://supabase.com/llms/cli.txt` — see `docs-lookup` skill |
|
|
93
|
+
|
|
94
|
+
## Execute SQL (Critical — MCP's Most Used Tool)
|
|
95
|
+
|
|
96
|
+
Three methods, in order of preference:
|
|
97
|
+
|
|
98
|
+
### Method 1: Management API (recommended for remote)
|
|
99
|
+
```bash
|
|
100
|
+
# Set once per session
|
|
101
|
+
SUPA_API="https://api.supabase.com"
|
|
102
|
+
REF="your-project-ref" # Get from `supabase status` or `supabase projects list`
|
|
103
|
+
|
|
104
|
+
# Execute any SQL
|
|
105
|
+
curl -s -X POST "$SUPA_API/v1/projects/$REF/database/query" \
|
|
106
|
+
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
|
107
|
+
-H "Content-Type: application/json" \
|
|
108
|
+
-d '{"query": "SELECT * FROM users LIMIT 10"}'
|
|
109
|
+
|
|
110
|
+
# Read-only query (safer)
|
|
111
|
+
curl -s -X POST "$SUPA_API/v1/projects/$REF/database/query" \
|
|
112
|
+
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
|
113
|
+
-H "Content-Type: application/json" \
|
|
114
|
+
-d '{"query": "SELECT * FROM users LIMIT 10", "read_only": true}'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Method 2: psql (if connection string available)
|
|
118
|
+
```bash
|
|
119
|
+
# Connection string from supabase status or .env
|
|
120
|
+
psql "postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres" \
|
|
121
|
+
-c "SELECT * FROM users LIMIT 10"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Method 3: Migration (for DDL/schema changes)
|
|
125
|
+
```bash
|
|
126
|
+
supabase migration new my_change
|
|
127
|
+
# Edit the SQL file, then:
|
|
128
|
+
supabase db push
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Service Logs (Beyond Edge Functions)
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Edge function logs (CLI)
|
|
135
|
+
supabase functions logs <name> --tail
|
|
136
|
+
|
|
137
|
+
# Other service logs (API)
|
|
138
|
+
curl -s "$SUPA_API/v1/projects/$REF/analytics/endpoints/logs.all?iso_timestamp_start=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
|
|
139
|
+
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Database Inspection (13 commands — MCP had 0)
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
supabase inspect db table-stats # Row counts, sizes
|
|
146
|
+
supabase inspect db index-stats # Index usage
|
|
147
|
+
supabase inspect db cache-hit # Buffer cache hit rates
|
|
148
|
+
supabase inspect db outliers # Slowest queries
|
|
149
|
+
supabase inspect db blocking # Lock contention
|
|
150
|
+
supabase inspect db locks # Exclusive locks
|
|
151
|
+
supabase inspect db long-running-queries # Queries > 5min
|
|
152
|
+
supabase inspect db bloat # Table/index bloat
|
|
153
|
+
supabase inspect db vacuum-stats # Autovacuum stats
|
|
154
|
+
supabase inspect db calls # Query frequency
|
|
155
|
+
supabase inspect db db-stats # Database stats
|
|
156
|
+
supabase inspect db replication-slots # Replication health
|
|
157
|
+
supabase inspect db traffic-profile # Read/write ratios
|
|
158
|
+
supabase inspect report # Full CSV report
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Environment Variable
|
|
162
|
+
|
|
163
|
+
The Management API requires `SUPABASE_ACCESS_TOKEN`. Set it:
|
|
164
|
+
```bash
|
|
165
|
+
export SUPABASE_ACCESS_TOKEN="sbp_..." # Personal access token from supabase.com/dashboard/account/tokens
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Shorthand used above: `SUPA_API="https://api.supabase.com"`, `REF="project-ref"`
|
|
169
|
+
|
|
170
|
+
## Key Rules
|
|
171
|
+
|
|
172
|
+
- ALWAYS enable RLS on every table
|
|
173
|
+
- ALWAYS write policies checking `auth.uid()`
|
|
174
|
+
- Never expose `service_role` key client-side
|
|
175
|
+
- Use `maybeSingle()` not `single()` when row might not exist
|
|
176
|
+
- Validate with Zod. Use PKCE auth flow for SSR/mobile.
|
|
177
|
+
|
|
178
|
+
## References
|
|
179
|
+
|
|
180
|
+
- Code templates: read `@.claude/skills/supabase/references/templates.md`
|
|
181
|
+
- Latest CLI docs: `WebFetch: https://supabase.com/llms/cli.txt`
|
|
182
|
+
- Management API: `WebFetch: https://supabase.com/docs/reference/api/introduction`
|
|
183
|
+
|
|
184
|
+
## Trigger Phrases
|
|
185
|
+
|
|
186
|
+
- "create table" / "debug RLS" / "optimize queries" / "inspect db"
|
|
187
|
+
- "supabase auth" / "edge function" / "deploy function" / "migration"
|
|
188
|
+
- "run SQL" / "query database" / "check logs" / "storage"
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## When Things Go Wrong
|
|
193
|
+
|
|
194
|
+
### Migration Fails
|
|
195
|
+
|
|
196
|
+
**Syntax error in SQL:**
|
|
197
|
+
```bash
|
|
198
|
+
supabase db push 2>&1 # Read the exact Postgres error
|
|
199
|
+
# Fix the SQL file in supabase/migrations/, then retry:
|
|
200
|
+
supabase db push
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Conflicting schema (column/table already exists):**
|
|
204
|
+
```bash
|
|
205
|
+
# Check what's actually in the remote database
|
|
206
|
+
supabase db dump --schema public --project-ref <ref> | grep "table_or_column_name"
|
|
207
|
+
# If migration is stale, mark it as applied without running:
|
|
208
|
+
supabase migration repair <version> --status applied
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**RLS blocks the migration (permission denied during ALTER):**
|
|
212
|
+
Migrations run as the `postgres` superuser, so RLS shouldn't block DDL. If you see permission errors:
|
|
213
|
+
```bash
|
|
214
|
+
# Check if the migration is trying DML (INSERT/UPDATE) on an RLS-enabled table
|
|
215
|
+
# Fix: Use service_role or temporarily disable RLS in the migration:
|
|
216
|
+
ALTER TABLE my_table DISABLE ROW LEVEL SECURITY;
|
|
217
|
+
-- ... do the data migration ...
|
|
218
|
+
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Migration Applied But Needs Rollback
|
|
222
|
+
|
|
223
|
+
Supabase has no built-in `migrate down`. The process is manual:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# 1. Create a reverse migration
|
|
227
|
+
supabase migration new rollback_my_change
|
|
228
|
+
|
|
229
|
+
# 2. Write the inverse SQL
|
|
230
|
+
# Added a column? → ALTER TABLE x DROP COLUMN y;
|
|
231
|
+
# Created a table? → DROP TABLE x;
|
|
232
|
+
# Changed a type? → ALTER TABLE x ALTER COLUMN y TYPE old_type;
|
|
233
|
+
|
|
234
|
+
# 3. Push the reverse migration
|
|
235
|
+
supabase db push --project-ref <ref>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For data-destructive rollbacks (dropped column with data):
|
|
239
|
+
- If you have a backup: restore from Supabase dashboard → Database → Backups
|
|
240
|
+
- If no backup: the data is gone. This is why you test migrations on a branch first:
|
|
241
|
+
```bash
|
|
242
|
+
supabase branches create test-migration
|
|
243
|
+
# Test there, then merge or delete
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### RLS Policy Silently Blocking Queries
|
|
247
|
+
|
|
248
|
+
Symptoms: Query returns empty results, no error. The data exists but RLS filters it out.
|
|
249
|
+
|
|
250
|
+
**Step 1: Confirm it's RLS (bypass with service_role):**
|
|
251
|
+
```bash
|
|
252
|
+
# This uses service_role which bypasses RLS
|
|
253
|
+
curl -s "https://<ref>.supabase.co/rest/v1/my_table?select=*&limit=5" \
|
|
254
|
+
-H "apikey: <service_role_key>" \
|
|
255
|
+
-H "Authorization: Bearer <service_role_key>"
|
|
256
|
+
# If this returns data but your app doesn't → it's RLS
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Step 2: Check existing policies:**
|
|
260
|
+
```sql
|
|
261
|
+
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
|
|
262
|
+
FROM pg_policies
|
|
263
|
+
WHERE tablename = 'my_table';
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Step 3: Test the policy logic:**
|
|
267
|
+
```sql
|
|
268
|
+
-- Simulate as a specific user
|
|
269
|
+
SET request.jwt.claim.sub = 'user-uuid-here';
|
|
270
|
+
SET role authenticated;
|
|
271
|
+
SELECT * FROM my_table; -- Does this return the expected rows?
|
|
272
|
+
RESET role;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Common RLS bugs:**
|
|
276
|
+
- Policy uses `auth.uid()` but the user isn't authenticated (anon request)
|
|
277
|
+
- Policy checks a column that's NULL (NULL != NULL in Postgres, use `IS NOT DISTINCT FROM`)
|
|
278
|
+
- Missing policy for the specific operation (e.g., SELECT policy exists but not INSERT)
|
|
279
|
+
- `FOR ALL` policy doesn't cover all operations — it's actually `USING` only, add `WITH CHECK` for writes
|
|
280
|
+
|
|
281
|
+
### Edge Function Deployment Fails
|
|
282
|
+
|
|
283
|
+
**Import errors (module not found in Deno):**
|
|
284
|
+
```bash
|
|
285
|
+
supabase functions deploy my-function 2>&1
|
|
286
|
+
# Common fix: use full URLs for Deno imports
|
|
287
|
+
# Wrong: import { z } from "zod"
|
|
288
|
+
# Right: import { z } from "https://deno.land/x/zod/mod.ts"
|
|
289
|
+
# Or use import_map.json in supabase/functions/
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Missing env vars in Edge Functions:**
|
|
293
|
+
```bash
|
|
294
|
+
# List secrets
|
|
295
|
+
supabase secrets list
|
|
296
|
+
# Set missing secrets
|
|
297
|
+
supabase secrets set MY_VAR=my_value
|
|
298
|
+
# Required for most functions:
|
|
299
|
+
supabase secrets set SUPABASE_URL=https://<ref>.supabase.co
|
|
300
|
+
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=<key>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Cold start timeout (function takes > 10s on first call):**
|
|
304
|
+
- Keep functions small — large bundles = slow cold starts
|
|
305
|
+
- Avoid heavy imports at module level (lazy-load if possible)
|
|
306
|
+
- Use `supabase functions serve` locally to test performance
|
|
307
|
+
- For critical paths: set up a cron to warm the function every 5 min
|
|
308
|
+
|
|
309
|
+
**Deployment hangs or times out:**
|
|
310
|
+
```bash
|
|
311
|
+
# Check function size
|
|
312
|
+
du -sh supabase/functions/my-function/
|
|
313
|
+
# If > 10MB, you're importing too much. Trim dependencies.
|
|
314
|
+
# Retry deployment:
|
|
315
|
+
supabase functions deploy my-function --no-verify-jwt # If JWT verify is blocking
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Connection Pool Exhausted
|
|
319
|
+
|
|
320
|
+
Symptoms: `FATAL: too many connections for role "postgres"` or queries timing out intermittently.
|
|
321
|
+
|
|
322
|
+
**Diagnose:**
|
|
323
|
+
```bash
|
|
324
|
+
# Check active connections
|
|
325
|
+
supabase inspect db connections # If available
|
|
326
|
+
# Or via SQL:
|
|
327
|
+
# SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
|
|
328
|
+
# SELECT count(*) FROM pg_stat_activity;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Fix:**
|
|
332
|
+
1. Use the **pooler** connection string (port 6543) instead of direct (port 5432)
|
|
333
|
+
2. In your app: ensure you're using a single Supabase client instance, not creating one per request
|
|
334
|
+
3. Close idle connections:
|
|
335
|
+
```sql
|
|
336
|
+
SELECT pg_terminate_backend(pid)
|
|
337
|
+
FROM pg_stat_activity
|
|
338
|
+
WHERE state = 'idle'
|
|
339
|
+
AND query_start < NOW() - INTERVAL '5 minutes';
|
|
340
|
+
```
|
|
341
|
+
4. If on a small plan: upgrade compute or reduce max connections per service
|
|
342
|
+
|
|
343
|
+
### Type Generation Fails After Migration
|
|
344
|
+
|
|
345
|
+
Symptoms: `supabase gen types typescript` returns stale types or errors.
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# Force refresh (skip cache)
|
|
349
|
+
supabase gen types typescript --linked > types/supabase.ts
|
|
350
|
+
|
|
351
|
+
# If it still shows old schema, the migration might not be applied:
|
|
352
|
+
supabase migration list --project-ref <ref>
|
|
353
|
+
|
|
354
|
+
# If migration is applied but types are stale:
|
|
355
|
+
supabase db push --project-ref <ref> # Ensure remote is synced
|
|
356
|
+
supabase gen types typescript --project-id <ref> > types/supabase.ts
|
|
357
|
+
|
|
358
|
+
# Nuclear option: regenerate from scratch
|
|
359
|
+
rm types/supabase.ts
|
|
360
|
+
supabase gen types typescript --linked > types/supabase.ts
|
|
361
|
+
npx tsc --noEmit # Verify types compile
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Management API Timeout or 500
|
|
365
|
+
|
|
366
|
+
The Management API (`api.supabase.com`) occasionally has issues.
|
|
367
|
+
|
|
368
|
+
**Retry strategy:**
|
|
369
|
+
```bash
|
|
370
|
+
# Simple retry with backoff
|
|
371
|
+
for i in 1 2 3; do
|
|
372
|
+
result=$(curl -s -w "%{http_code}" -o /tmp/supa-response \
|
|
373
|
+
"$SUPA_API/v1/projects/$REF/database/query" \
|
|
374
|
+
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
|
|
375
|
+
-H "Content-Type: application/json" \
|
|
376
|
+
-d '{"query": "SELECT 1"}')
|
|
377
|
+
if [[ "$result" == "200" ]]; then cat /tmp/supa-response; break; fi
|
|
378
|
+
echo "Attempt $i failed ($result), retrying..."
|
|
379
|
+
sleep $((i * 5))
|
|
380
|
+
done
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Fallback to CLI:**
|
|
384
|
+
```bash
|
|
385
|
+
# If Management API is down, use CLI directly (uses different endpoints)
|
|
386
|
+
supabase db execute --project-ref <ref> "SELECT 1"
|
|
387
|
+
# Or connect via psql if you have the connection string
|
|
388
|
+
psql "$DATABASE_URL" -c "SELECT 1"
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Check Supabase status:** https://status.supabase.com
|
|
392
|
+
|
|
393
|
+
## Multi-Role RLS Policies
|
|
394
|
+
|
|
395
|
+
Sakani-style multi-role RBAC where roles are stored in separate tables (not JWT claims). This pattern supports 8 distinct roles:
|
|
396
|
+
|
|
397
|
+
**Building-scoped roles** (in `building_role_assignment` table):
|
|
398
|
+
- `BM_VERIFIED` — verified building manager
|
|
399
|
+
- `BM_CANDIDATE` — manager in review
|
|
400
|
+
- `GUARD` — building security guard
|
|
401
|
+
- `DEV_ADMIN` — staff admin (platform level)
|
|
402
|
+
|
|
403
|
+
**Unit-scoped roles** (in `unit_membership` table):
|
|
404
|
+
- `OWNER_VERIFIED` — verified unit owner
|
|
405
|
+
- `TENANT` — active tenant
|
|
406
|
+
|
|
407
|
+
All policies check `auth.uid()` and require an active role status. **Deny by default**: no permissive policies without explicit role validation.
|
|
408
|
+
|
|
409
|
+
### Unit-Scoped Read (Owner/Tenant)
|
|
410
|
+
|
|
411
|
+
Unit members can read data for their own unit:
|
|
412
|
+
|
|
413
|
+
```sql
|
|
414
|
+
CREATE POLICY "unit_members_read_own" ON target_table
|
|
415
|
+
FOR SELECT USING (
|
|
416
|
+
EXISTS (
|
|
417
|
+
SELECT 1 FROM unit_membership um
|
|
418
|
+
WHERE um.unit_id = target_table.unit_id
|
|
419
|
+
AND um.user_id = auth.uid()
|
|
420
|
+
AND um.status = 'ACTIVE'
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Building-Scoped Read (Building Manager)
|
|
426
|
+
|
|
427
|
+
Building managers (verified or candidate) can read all data in their assigned building:
|
|
428
|
+
|
|
429
|
+
```sql
|
|
430
|
+
CREATE POLICY "bm_read_building" ON target_table
|
|
431
|
+
FOR SELECT USING (
|
|
432
|
+
EXISTS (
|
|
433
|
+
SELECT 1 FROM building_role_assignment bra
|
|
434
|
+
WHERE bra.building_id = target_table.building_id
|
|
435
|
+
AND bra.user_id = auth.uid()
|
|
436
|
+
AND bra.role IN ('BM_VERIFIED', 'BM_CANDIDATE')
|
|
437
|
+
AND bra.status = 'ACTIVE'
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Guard-Scoped Read (Assigned Guard)
|
|
443
|
+
|
|
444
|
+
Guards can read only items explicitly assigned to them, within their building:
|
|
445
|
+
|
|
446
|
+
```sql
|
|
447
|
+
CREATE POLICY "guard_read_assigned" ON target_table
|
|
448
|
+
FOR SELECT USING (
|
|
449
|
+
target_table.assigned_to = auth.uid()
|
|
450
|
+
AND EXISTS (
|
|
451
|
+
SELECT 1 FROM building_role_assignment bra
|
|
452
|
+
WHERE bra.building_id = target_table.building_id
|
|
453
|
+
AND bra.user_id = auth.uid()
|
|
454
|
+
AND bra.role = 'GUARD'
|
|
455
|
+
AND bra.status = 'ACTIVE'
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Staff Admin Global Read
|
|
461
|
+
|
|
462
|
+
Platform staff admins can read all data across all buildings:
|
|
463
|
+
|
|
464
|
+
```sql
|
|
465
|
+
CREATE POLICY "staff_read_all" ON target_table
|
|
466
|
+
FOR SELECT USING (
|
|
467
|
+
EXISTS (
|
|
468
|
+
SELECT 1 FROM building_role_assignment bra
|
|
469
|
+
WHERE bra.user_id = auth.uid()
|
|
470
|
+
AND bra.role = 'DEV_ADMIN'
|
|
471
|
+
AND bra.status = 'ACTIVE'
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Write Policies
|
|
477
|
+
|
|
478
|
+
Write operations are more restrictive. Examples:
|
|
479
|
+
|
|
480
|
+
**Owners create for their own unit:**
|
|
481
|
+
```sql
|
|
482
|
+
CREATE POLICY "owner_create_own_unit" ON target_table
|
|
483
|
+
FOR INSERT WITH CHECK (
|
|
484
|
+
EXISTS (
|
|
485
|
+
SELECT 1 FROM unit_membership um
|
|
486
|
+
WHERE um.unit_id = target_table.unit_id
|
|
487
|
+
AND um.user_id = auth.uid()
|
|
488
|
+
AND um.role = 'OWNER_VERIFIED'
|
|
489
|
+
AND um.status = 'ACTIVE'
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Building managers create for their building:**
|
|
495
|
+
```sql
|
|
496
|
+
CREATE POLICY "bm_create_building" ON target_table
|
|
497
|
+
FOR INSERT WITH CHECK (
|
|
498
|
+
EXISTS (
|
|
499
|
+
SELECT 1 FROM building_role_assignment bra
|
|
500
|
+
WHERE bra.building_id = target_table.building_id
|
|
501
|
+
AND bra.user_id = auth.uid()
|
|
502
|
+
AND bra.role IN ('BM_VERIFIED', 'BM_CANDIDATE')
|
|
503
|
+
AND bra.status = 'ACTIVE'
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**Guards update their assigned items:**
|
|
509
|
+
```sql
|
|
510
|
+
CREATE POLICY "guard_update_assigned" ON target_table
|
|
511
|
+
FOR UPDATE USING (
|
|
512
|
+
target_table.assigned_to = auth.uid()
|
|
513
|
+
AND EXISTS (
|
|
514
|
+
SELECT 1 FROM building_role_assignment bra
|
|
515
|
+
WHERE bra.building_id = target_table.building_id
|
|
516
|
+
AND bra.user_id = auth.uid()
|
|
517
|
+
AND bra.role = 'GUARD'
|
|
518
|
+
AND bra.status = 'ACTIVE'
|
|
519
|
+
)
|
|
520
|
+
) WITH CHECK (
|
|
521
|
+
target_table.assigned_to = auth.uid()
|
|
522
|
+
);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Composite Multi-Role Example
|
|
526
|
+
|
|
527
|
+
A ticket table where owners create for their unit, building managers read all in building, guards read assigned, staff reads all:
|
|
528
|
+
|
|
529
|
+
```sql
|
|
530
|
+
-- READ: Unit members (owners/tenants)
|
|
531
|
+
CREATE POLICY "ticket_unit_read" ON ticket
|
|
532
|
+
FOR SELECT USING (
|
|
533
|
+
EXISTS (
|
|
534
|
+
SELECT 1 FROM unit_membership um
|
|
535
|
+
WHERE um.unit_id = ticket.unit_id
|
|
536
|
+
AND um.user_id = auth.uid()
|
|
537
|
+
AND um.status = 'ACTIVE'
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
-- READ: Building managers
|
|
542
|
+
CREATE POLICY "ticket_bm_read" ON ticket
|
|
543
|
+
FOR SELECT USING (
|
|
544
|
+
EXISTS (
|
|
545
|
+
SELECT 1 FROM building_role_assignment bra
|
|
546
|
+
WHERE bra.building_id = (
|
|
547
|
+
SELECT building_id FROM unit WHERE id = ticket.unit_id
|
|
548
|
+
)
|
|
549
|
+
AND bra.user_id = auth.uid()
|
|
550
|
+
AND bra.role IN ('BM_VERIFIED', 'BM_CANDIDATE')
|
|
551
|
+
AND bra.status = 'ACTIVE'
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
-- READ: Assigned guards
|
|
556
|
+
CREATE POLICY "ticket_guard_read" ON ticket
|
|
557
|
+
FOR SELECT USING (
|
|
558
|
+
ticket.assigned_guard = auth.uid()
|
|
559
|
+
AND EXISTS (
|
|
560
|
+
SELECT 1 FROM building_role_assignment bra
|
|
561
|
+
WHERE bra.building_id = (
|
|
562
|
+
SELECT building_id FROM unit WHERE id = ticket.unit_id
|
|
563
|
+
)
|
|
564
|
+
AND bra.user_id = auth.uid()
|
|
565
|
+
AND bra.role = 'GUARD'
|
|
566
|
+
AND bra.status = 'ACTIVE'
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
-- READ: Staff admin global
|
|
571
|
+
CREATE POLICY "ticket_staff_read" ON ticket
|
|
572
|
+
FOR SELECT USING (
|
|
573
|
+
EXISTS (
|
|
574
|
+
SELECT 1 FROM building_role_assignment bra
|
|
575
|
+
WHERE bra.user_id = auth.uid()
|
|
576
|
+
AND bra.role = 'DEV_ADMIN'
|
|
577
|
+
AND bra.status = 'ACTIVE'
|
|
578
|
+
)
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
-- CREATE: Unit owners only
|
|
582
|
+
CREATE POLICY "ticket_owner_create" ON ticket
|
|
583
|
+
FOR INSERT WITH CHECK (
|
|
584
|
+
EXISTS (
|
|
585
|
+
SELECT 1 FROM unit_membership um
|
|
586
|
+
WHERE um.unit_id = ticket.unit_id
|
|
587
|
+
AND um.user_id = auth.uid()
|
|
588
|
+
AND um.role = 'OWNER_VERIFIED'
|
|
589
|
+
AND um.status = 'ACTIVE'
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
-- UPDATE: Assigned guards
|
|
594
|
+
CREATE POLICY "ticket_guard_update" ON ticket
|
|
595
|
+
FOR UPDATE USING (
|
|
596
|
+
ticket.assigned_guard = auth.uid()
|
|
597
|
+
) WITH CHECK (
|
|
598
|
+
ticket.assigned_guard = auth.uid()
|
|
599
|
+
);
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Financial Table Patterns
|
|
603
|
+
|
|
604
|
+
Append-only financial ledger tables enforce immutability at the database level, preventing accidental or malicious modifications. This pattern ensures compliance and audit integrity.
|
|
605
|
+
|
|
606
|
+
### Append-Only Table Setup
|
|
607
|
+
|
|
608
|
+
Create a financial table with NO update/delete permissions:
|
|
609
|
+
|
|
610
|
+
```sql
|
|
611
|
+
CREATE TABLE ledger_entry (
|
|
612
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
613
|
+
building_id UUID NOT NULL REFERENCES building(id) ON DELETE RESTRICT,
|
|
614
|
+
unit_id UUID REFERENCES unit(id) ON DELETE RESTRICT,
|
|
615
|
+
account_id UUID NOT NULL REFERENCES account(id) ON DELETE RESTRICT,
|
|
616
|
+
|
|
617
|
+
-- Financial data
|
|
618
|
+
entry_type TEXT NOT NULL CHECK (entry_type IN ('CHARGE', 'PAYMENT', 'REVERSAL', 'ADJUSTMENT')),
|
|
619
|
+
amount NUMERIC NOT NULL,
|
|
620
|
+
currency TEXT NOT NULL DEFAULT 'JOD',
|
|
621
|
+
description TEXT,
|
|
622
|
+
|
|
623
|
+
-- Idempotency
|
|
624
|
+
request_id UUID NOT NULL UNIQUE,
|
|
625
|
+
|
|
626
|
+
-- Audit
|
|
627
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
628
|
+
created_by UUID NOT NULL REFERENCES auth.users(id),
|
|
629
|
+
|
|
630
|
+
-- Indexes for RLS and queries
|
|
631
|
+
CONSTRAINT positive_amount CHECK (amount > 0 OR entry_type = 'REVERSAL')
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
-- Enable RLS
|
|
635
|
+
ALTER TABLE ledger_entry ENABLE ROW LEVEL SECURITY;
|
|
636
|
+
|
|
637
|
+
-- Add indexes for common queries
|
|
638
|
+
CREATE INDEX idx_ledger_building_date ON ledger_entry(building_id, created_at DESC);
|
|
639
|
+
CREATE INDEX idx_ledger_account_date ON ledger_entry(account_id, created_at DESC);
|
|
640
|
+
CREATE INDEX idx_ledger_request_id ON ledger_entry(request_id);
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Prevent Mutations with Trigger
|
|
644
|
+
|
|
645
|
+
A trigger enforces append-only behavior even for the `service_role`:
|
|
646
|
+
|
|
647
|
+
```sql
|
|
648
|
+
CREATE OR REPLACE FUNCTION prevent_ledger_mutation()
|
|
649
|
+
RETURNS TRIGGER AS $$
|
|
650
|
+
BEGIN
|
|
651
|
+
RAISE EXCEPTION 'ledger_entry is append-only. Use REVERSAL entries for corrections.';
|
|
652
|
+
END;
|
|
653
|
+
$$ LANGUAGE plpgsql;
|
|
654
|
+
|
|
655
|
+
CREATE TRIGGER enforce_append_only
|
|
656
|
+
BEFORE UPDATE OR DELETE ON ledger_entry
|
|
657
|
+
FOR EACH ROW EXECUTE FUNCTION prevent_ledger_mutation();
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Idempotency Key Pattern
|
|
661
|
+
|
|
662
|
+
Use `request_id` with a UNIQUE constraint to ensure duplicate requests produce the same entry:
|
|
663
|
+
|
|
664
|
+
```sql
|
|
665
|
+
-- Insert with idempotency key
|
|
666
|
+
INSERT INTO ledger_entry (
|
|
667
|
+
building_id, account_id, entry_type, amount, currency,
|
|
668
|
+
description, request_id, created_by
|
|
669
|
+
)
|
|
670
|
+
VALUES (
|
|
671
|
+
'bldg-123', 'acct-456', 'CHARGE', 150.00, 'JOD',
|
|
672
|
+
'Monthly rent', 'req-789', auth.uid()
|
|
673
|
+
)
|
|
674
|
+
ON CONFLICT (request_id) DO NOTHING
|
|
675
|
+
RETURNING *;
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
If the same `request_id` is submitted again, the row is silently skipped (idempotent). Check the result count to detect duplicates.
|
|
679
|
+
|
|
680
|
+
### Standard Audit Columns
|
|
681
|
+
|
|
682
|
+
All financial tables should include:
|
|
683
|
+
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — exact timestamp of entry creation
|
|
684
|
+
- `created_by UUID NOT NULL REFERENCES auth.users(id)` — who created the entry
|
|
685
|
+
- `request_id UUID NOT NULL UNIQUE` — external transaction ID for idempotency
|
|
686
|
+
|
|
687
|
+
### Monetary Value Conventions
|
|
688
|
+
|
|
689
|
+
- Use `NUMERIC` (not `FLOAT`) for all amounts to avoid floating-point errors:
|
|
690
|
+
```sql
|
|
691
|
+
amount NUMERIC(10,2) NOT NULL
|
|
692
|
+
```
|
|
693
|
+
- Always store currency explicitly (default to 'JOD' for Sakani):
|
|
694
|
+
```sql
|
|
695
|
+
currency TEXT NOT NULL DEFAULT 'JOD'
|
|
696
|
+
```
|
|
697
|
+
- Enforce positive amounts with CHECK constraints:
|
|
698
|
+
```sql
|
|
699
|
+
CONSTRAINT amount_positive CHECK (amount > 0 OR entry_type IN ('REVERSAL', 'ADJUSTMENT'))
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## Audit Logging
|
|
703
|
+
|
|
704
|
+
Immutable audit logs track all critical changes for compliance and forensic analysis.
|
|
705
|
+
|
|
706
|
+
### Immutable Audit Table
|
|
707
|
+
|
|
708
|
+
```sql
|
|
709
|
+
CREATE TABLE audit_log (
|
|
710
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
711
|
+
building_id UUID NOT NULL REFERENCES building(id) ON DELETE RESTRICT,
|
|
712
|
+
|
|
713
|
+
-- What changed
|
|
714
|
+
table_name TEXT NOT NULL,
|
|
715
|
+
record_id UUID NOT NULL,
|
|
716
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('INSERT', 'UPDATE', 'DELETE', 'BULK_OPERATION')),
|
|
717
|
+
|
|
718
|
+
-- Who, when
|
|
719
|
+
user_id UUID NOT NULL REFERENCES auth.users(id),
|
|
720
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
721
|
+
|
|
722
|
+
-- Change details (JSON for flexibility)
|
|
723
|
+
old_values JSONB,
|
|
724
|
+
new_values JSONB,
|
|
725
|
+
change_summary TEXT,
|
|
726
|
+
|
|
727
|
+
-- Context
|
|
728
|
+
request_id UUID,
|
|
729
|
+
ip_address INET,
|
|
730
|
+
user_agent TEXT
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
-- Enable RLS (users see only their building's logs if managers)
|
|
734
|
+
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
|
|
735
|
+
|
|
736
|
+
-- Prevent mutations
|
|
737
|
+
CREATE TRIGGER audit_log_immutable
|
|
738
|
+
BEFORE UPDATE OR DELETE ON audit_log
|
|
739
|
+
FOR EACH ROW EXECUTE FUNCTION prevent_ledger_mutation();
|
|
740
|
+
|
|
741
|
+
-- Indexes for efficient querying
|
|
742
|
+
CREATE INDEX idx_audit_building_date ON audit_log(building_id, created_at DESC);
|
|
743
|
+
CREATE INDEX idx_audit_event_date ON audit_log(event_type, created_at DESC);
|
|
744
|
+
CREATE INDEX idx_audit_user ON audit_log(user_id, created_at DESC);
|
|
745
|
+
CREATE INDEX idx_audit_record ON audit_log(table_name, record_id, created_at DESC);
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### Trigger-Based Logging
|
|
749
|
+
|
|
750
|
+
Automatically log changes to a critical table:
|
|
751
|
+
|
|
752
|
+
```sql
|
|
753
|
+
CREATE OR REPLACE FUNCTION log_critical_changes()
|
|
754
|
+
RETURNS TRIGGER AS $$
|
|
755
|
+
BEGIN
|
|
756
|
+
INSERT INTO audit_log (
|
|
757
|
+
building_id, table_name, record_id, event_type,
|
|
758
|
+
user_id, old_values, new_values, change_summary
|
|
759
|
+
)
|
|
760
|
+
VALUES (
|
|
761
|
+
COALESCE(NEW.building_id, OLD.building_id),
|
|
762
|
+
TG_TABLE_NAME,
|
|
763
|
+
COALESCE(NEW.id, OLD.id),
|
|
764
|
+
TG_OP,
|
|
765
|
+
auth.uid(),
|
|
766
|
+
TO_JSONB(OLD),
|
|
767
|
+
TO_JSONB(NEW),
|
|
768
|
+
CASE TG_OP
|
|
769
|
+
WHEN 'DELETE' THEN 'Record deleted'
|
|
770
|
+
WHEN 'UPDATE' THEN 'Record updated: ' || (
|
|
771
|
+
SELECT STRING_AGG(key || ' changed', ', ')
|
|
772
|
+
FROM (SELECT key FROM JSONB_EACH(TO_JSONB(NEW)) n
|
|
773
|
+
WHERE NOT (TO_JSONB(OLD) -> key IS DISTINCT FROM n.value)) t
|
|
774
|
+
)
|
|
775
|
+
WHEN 'INSERT' THEN 'Record created'
|
|
776
|
+
END
|
|
777
|
+
);
|
|
778
|
+
RETURN COALESCE(NEW, OLD);
|
|
779
|
+
END;
|
|
780
|
+
$$ LANGUAGE plpgsql;
|
|
781
|
+
|
|
782
|
+
-- Attach to any critical table
|
|
783
|
+
CREATE TRIGGER audit_unit_changes
|
|
784
|
+
AFTER INSERT OR UPDATE OR DELETE ON unit
|
|
785
|
+
FOR EACH ROW EXECUTE FUNCTION log_critical_changes();
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Retention Policy
|
|
789
|
+
|
|
790
|
+
Financial and audit logs should be retained for **7 years** per most jurisdictions:
|
|
791
|
+
|
|
792
|
+
```sql
|
|
793
|
+
-- Cleanup job (run via cron)
|
|
794
|
+
DELETE FROM audit_log
|
|
795
|
+
WHERE created_at < NOW() - INTERVAL '7 years'
|
|
796
|
+
AND building_id IN (
|
|
797
|
+
SELECT id FROM building WHERE status = 'ARCHIVED'
|
|
798
|
+
);
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
## Multi-Tenant Migration Patterns
|
|
802
|
+
|
|
803
|
+
Schema migrations for multi-tenant databases require careful planning to maintain data integrity and RLS security.
|
|
804
|
+
|
|
805
|
+
### Enable RLS Immediately After CREATE TABLE
|
|
806
|
+
|
|
807
|
+
Never create a table without RLS:
|
|
808
|
+
|
|
809
|
+
```sql
|
|
810
|
+
CREATE TABLE my_table (
|
|
811
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
812
|
+
building_id UUID NOT NULL REFERENCES building(id),
|
|
813
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
-- IMMEDIATELY enable RLS
|
|
817
|
+
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
|
|
818
|
+
|
|
819
|
+
-- Then define policies
|
|
820
|
+
CREATE POLICY "building_read" ON my_table
|
|
821
|
+
FOR SELECT USING (
|
|
822
|
+
EXISTS (
|
|
823
|
+
SELECT 1 FROM building_role_assignment bra
|
|
824
|
+
WHERE bra.building_id = my_table.building_id
|
|
825
|
+
AND bra.user_id = auth.uid()
|
|
826
|
+
AND bra.status = 'ACTIVE'
|
|
827
|
+
)
|
|
828
|
+
);
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Create Enum Types Before Tables
|
|
832
|
+
|
|
833
|
+
Define enums once, before any table references them:
|
|
834
|
+
|
|
835
|
+
```sql
|
|
836
|
+
CREATE TYPE building_role AS ENUM (
|
|
837
|
+
'BM_VERIFIED', 'BM_CANDIDATE', 'GUARD', 'DEV_ADMIN'
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
CREATE TYPE membership_status AS ENUM (
|
|
841
|
+
'ACTIVE', 'INACTIVE', 'SUSPENDED'
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
-- Now create tables that use these enums
|
|
845
|
+
CREATE TABLE building_role_assignment (
|
|
846
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
847
|
+
building_id UUID NOT NULL REFERENCES building(id),
|
|
848
|
+
user_id UUID NOT NULL REFERENCES auth.users(id),
|
|
849
|
+
role building_role NOT NULL,
|
|
850
|
+
status membership_status NOT NULL DEFAULT 'ACTIVE',
|
|
851
|
+
UNIQUE(building_id, user_id, role)
|
|
852
|
+
);
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Index FK Columns Used in RLS Policies
|
|
856
|
+
|
|
857
|
+
RLS policies with subqueries require indexes on the join columns for acceptable performance:
|
|
858
|
+
|
|
859
|
+
```sql
|
|
860
|
+
-- Without these indexes, every RLS check is a full table scan
|
|
861
|
+
CREATE INDEX idx_unit_membership_unit_user ON unit_membership(unit_id, user_id);
|
|
862
|
+
CREATE INDEX idx_building_role_assignment_building_user ON building_role_assignment(building_id, user_id);
|
|
863
|
+
CREATE INDEX idx_building_role_assignment_user ON building_role_assignment(user_id);
|
|
864
|
+
|
|
865
|
+
-- If status is checked in policies, include it in composite indexes
|
|
866
|
+
CREATE INDEX idx_building_role_status ON building_role_assignment(building_id, user_id, status);
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
### Complete Table Migration Example
|
|
870
|
+
|
|
871
|
+
A single migration creating a table with full RLS in one go:
|
|
872
|
+
|
|
873
|
+
```sql
|
|
874
|
+
-- supabase/migrations/20240306_create_ticket_system.sql
|
|
875
|
+
|
|
876
|
+
-- 1. Enums
|
|
877
|
+
CREATE TYPE ticket_status AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED');
|
|
878
|
+
CREATE TYPE ticket_priority AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
|
|
879
|
+
|
|
880
|
+
-- 2. Table with all constraints and defaults
|
|
881
|
+
CREATE TABLE ticket (
|
|
882
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
883
|
+
building_id UUID NOT NULL REFERENCES building(id) ON DELETE RESTRICT,
|
|
884
|
+
unit_id UUID NOT NULL REFERENCES unit(id) ON DELETE RESTRICT,
|
|
885
|
+
created_by UUID NOT NULL REFERENCES auth.users(id),
|
|
886
|
+
assigned_guard UUID REFERENCES auth.users(id),
|
|
887
|
+
|
|
888
|
+
title TEXT NOT NULL,
|
|
889
|
+
description TEXT,
|
|
890
|
+
status ticket_status NOT NULL DEFAULT 'OPEN',
|
|
891
|
+
priority ticket_priority NOT NULL DEFAULT 'MEDIUM',
|
|
892
|
+
|
|
893
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
894
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
895
|
+
resolved_at TIMESTAMPTZ,
|
|
896
|
+
|
|
897
|
+
CONSTRAINT title_not_empty CHECK (LENGTH(title) > 0)
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
-- 3. Indexes immediately (before RLS policies)
|
|
901
|
+
CREATE INDEX idx_ticket_building ON ticket(building_id);
|
|
902
|
+
CREATE INDEX idx_ticket_unit ON ticket(unit_id);
|
|
903
|
+
CREATE INDEX idx_ticket_created_by ON ticket(created_by);
|
|
904
|
+
CREATE INDEX idx_ticket_assigned_guard ON ticket(assigned_guard);
|
|
905
|
+
CREATE INDEX idx_ticket_status ON ticket(status, created_at DESC);
|
|
906
|
+
|
|
907
|
+
-- 4. Enable RLS
|
|
908
|
+
ALTER TABLE ticket ENABLE ROW LEVEL SECURITY;
|
|
909
|
+
|
|
910
|
+
-- 5. Define policies
|
|
911
|
+
CREATE POLICY "ticket_read_own_unit" ON ticket
|
|
912
|
+
FOR SELECT USING (
|
|
913
|
+
EXISTS (
|
|
914
|
+
SELECT 1 FROM unit_membership um
|
|
915
|
+
WHERE um.unit_id = ticket.unit_id
|
|
916
|
+
AND um.user_id = auth.uid()
|
|
917
|
+
AND um.status = 'ACTIVE'
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
CREATE POLICY "ticket_read_building" ON ticket
|
|
922
|
+
FOR SELECT USING (
|
|
923
|
+
EXISTS (
|
|
924
|
+
SELECT 1 FROM building_role_assignment bra
|
|
925
|
+
WHERE bra.building_id = ticket.building_id
|
|
926
|
+
AND bra.user_id = auth.uid()
|
|
927
|
+
AND bra.role IN ('BM_VERIFIED', 'BM_CANDIDATE')
|
|
928
|
+
AND bra.status = 'ACTIVE'
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
CREATE POLICY "ticket_read_assigned" ON ticket
|
|
933
|
+
FOR SELECT USING (
|
|
934
|
+
ticket.assigned_guard = auth.uid()
|
|
935
|
+
AND EXISTS (
|
|
936
|
+
SELECT 1 FROM building_role_assignment bra
|
|
937
|
+
WHERE bra.building_id = ticket.building_id
|
|
938
|
+
AND bra.user_id = auth.uid()
|
|
939
|
+
AND bra.role = 'GUARD'
|
|
940
|
+
AND bra.status = 'ACTIVE'
|
|
941
|
+
)
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
CREATE POLICY "ticket_create_owner" ON ticket
|
|
945
|
+
FOR INSERT WITH CHECK (
|
|
946
|
+
created_by = auth.uid()
|
|
947
|
+
AND EXISTS (
|
|
948
|
+
SELECT 1 FROM unit_membership um
|
|
949
|
+
WHERE um.unit_id = ticket.unit_id
|
|
950
|
+
AND um.user_id = auth.uid()
|
|
951
|
+
AND um.role = 'OWNER_VERIFIED'
|
|
952
|
+
AND um.status = 'ACTIVE'
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
CREATE POLICY "ticket_update_guard" ON ticket
|
|
957
|
+
FOR UPDATE USING (
|
|
958
|
+
ticket.assigned_guard = auth.uid()
|
|
959
|
+
) WITH CHECK (
|
|
960
|
+
ticket.assigned_guard = auth.uid()
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
-- 6. Grant default permissions
|
|
964
|
+
GRANT SELECT, INSERT ON ticket TO authenticated;
|
|
965
|
+
GRANT UPDATE ON ticket TO authenticated;
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Then push with:
|
|
969
|
+
```bash
|
|
970
|
+
supabase db push
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
---
|