@intentsolutionsio/supabase-pack 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +73 -47
  3. package/package.json +4 -4
  4. package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
  5. package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
  6. package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
  7. package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
  8. package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
  9. package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
  10. package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
  11. package/skills/supabase-architecture-variants/SKILL.md +395 -216
  12. package/skills/supabase-architecture-variants/references/errors.md +11 -0
  13. package/skills/supabase-architecture-variants/references/examples.md +12 -0
  14. package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
  15. package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
  16. package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
  17. package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
  18. package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
  19. package/skills/supabase-ci-integration/SKILL.md +315 -67
  20. package/skills/supabase-ci-integration/references/errors.md +10 -0
  21. package/skills/supabase-ci-integration/references/examples.md +36 -0
  22. package/skills/supabase-ci-integration/references/implementation.md +54 -0
  23. package/skills/supabase-common-errors/SKILL.md +320 -62
  24. package/skills/supabase-common-errors/references/errors.md +53 -0
  25. package/skills/supabase-common-errors/references/examples.md +23 -0
  26. package/skills/supabase-cost-tuning/SKILL.md +365 -131
  27. package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
  28. package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
  29. package/skills/supabase-cost-tuning/references/errors.md +11 -0
  30. package/skills/supabase-cost-tuning/references/examples.md +15 -0
  31. package/skills/supabase-data-handling/SKILL.md +378 -145
  32. package/skills/supabase-data-handling/references/errors.md +11 -0
  33. package/skills/supabase-data-handling/references/examples.md +27 -0
  34. package/skills/supabase-data-handling/references/implementation.md +223 -0
  35. package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
  36. package/skills/supabase-debug-bundle/SKILL.md +267 -73
  37. package/skills/supabase-debug-bundle/references/errors.md +12 -0
  38. package/skills/supabase-debug-bundle/references/examples.md +24 -0
  39. package/skills/supabase-debug-bundle/references/implementation.md +54 -0
  40. package/skills/supabase-deploy-integration/SKILL.md +258 -147
  41. package/skills/supabase-deploy-integration/references/errors.md +11 -0
  42. package/skills/supabase-deploy-integration/references/examples.md +21 -0
  43. package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
  44. package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
  45. package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
  46. package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
  47. package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
  48. package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
  49. package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
  50. package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
  51. package/skills/supabase-hello-world/SKILL.md +160 -54
  52. package/skills/supabase-incident-runbook/SKILL.md +453 -131
  53. package/skills/supabase-incident-runbook/references/errors.md +11 -0
  54. package/skills/supabase-incident-runbook/references/examples.md +10 -0
  55. package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
  56. package/skills/supabase-install-auth/SKILL.md +186 -50
  57. package/skills/supabase-install-auth/references/examples.md +102 -0
  58. package/skills/supabase-known-pitfalls/SKILL.md +411 -241
  59. package/skills/supabase-known-pitfalls/references/errors.md +11 -0
  60. package/skills/supabase-known-pitfalls/references/examples.md +12 -0
  61. package/skills/supabase-load-scale/SKILL.md +346 -217
  62. package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
  63. package/skills/supabase-load-scale/references/errors.md +11 -0
  64. package/skills/supabase-load-scale/references/examples.md +26 -0
  65. package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
  66. package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
  67. package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
  68. package/skills/supabase-local-dev-loop/SKILL.md +272 -73
  69. package/skills/supabase-local-dev-loop/references/errors.md +11 -0
  70. package/skills/supabase-local-dev-loop/references/examples.md +21 -0
  71. package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
  72. package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
  73. package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
  74. package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
  75. package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
  76. package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
  77. package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
  78. package/skills/supabase-multi-env-setup/SKILL.md +393 -152
  79. package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
  80. package/skills/supabase-multi-env-setup/references/errors.md +11 -0
  81. package/skills/supabase-multi-env-setup/references/examples.md +11 -0
  82. package/skills/supabase-observability/SKILL.md +318 -196
  83. package/skills/supabase-observability/references/alert-configuration.md +40 -0
  84. package/skills/supabase-observability/references/errors.md +11 -0
  85. package/skills/supabase-observability/references/examples.md +13 -0
  86. package/skills/supabase-observability/references/metrics-collection.md +65 -0
  87. package/skills/supabase-performance-tuning/SKILL.md +304 -160
  88. package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
  89. package/skills/supabase-performance-tuning/references/errors.md +11 -0
  90. package/skills/supabase-performance-tuning/references/examples.md +13 -0
  91. package/skills/supabase-policy-guardrails/SKILL.md +248 -221
  92. package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
  93. package/skills/supabase-policy-guardrails/references/errors.md +11 -0
  94. package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
  95. package/skills/supabase-policy-guardrails/references/examples.md +10 -0
  96. package/skills/supabase-prod-checklist/SKILL.md +474 -84
  97. package/skills/supabase-prod-checklist/references/errors.md +63 -0
  98. package/skills/supabase-prod-checklist/references/examples.md +153 -0
  99. package/skills/supabase-prod-checklist/references/implementation.md +113 -0
  100. package/skills/supabase-rate-limits/SKILL.md +311 -98
  101. package/skills/supabase-rate-limits/references/errors.md +11 -0
  102. package/skills/supabase-rate-limits/references/examples.md +46 -0
  103. package/skills/supabase-rate-limits/references/implementation.md +66 -0
  104. package/skills/supabase-reference-architecture/SKILL.md +249 -182
  105. package/skills/supabase-reference-architecture/references/errors.md +29 -0
  106. package/skills/supabase-reference-architecture/references/examples.md +116 -0
  107. package/skills/supabase-reference-architecture/references/key-components.md +244 -0
  108. package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
  109. package/skills/supabase-reliability-patterns/SKILL.md +229 -234
  110. package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
  111. package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
  112. package/skills/supabase-reliability-patterns/references/errors.md +11 -0
  113. package/skills/supabase-reliability-patterns/references/examples.md +11 -0
  114. package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
  115. package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
  116. package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
  117. package/skills/supabase-sdk-patterns/SKILL.md +388 -99
  118. package/skills/supabase-sdk-patterns/references/errors.md +11 -0
  119. package/skills/supabase-sdk-patterns/references/examples.md +45 -0
  120. package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
  121. package/skills/supabase-security-basics/SKILL.md +282 -102
  122. package/skills/supabase-security-basics/references/errors.md +10 -0
  123. package/skills/supabase-security-basics/references/examples.md +70 -0
  124. package/skills/supabase-security-basics/references/implementation.md +39 -0
  125. package/skills/supabase-upgrade-migration/SKILL.md +248 -66
  126. package/skills/supabase-upgrade-migration/references/errors.md +10 -0
  127. package/skills/supabase-upgrade-migration/references/examples.md +51 -0
  128. package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
  129. package/skills/supabase-webhooks-events/SKILL.md +412 -138
  130. package/skills/supabase-webhooks-events/references/errors.md +55 -0
  131. package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
  132. package/skills/supabase-webhooks-events/references/examples.md +133 -0
  133. package/skills/supabase-webhooks-events/references/signature-verification.md +165 -0
@@ -1,222 +1,389 @@
1
1
  ---
2
2
  name: supabase-enterprise-rbac
3
- description: |
4
- Configure Supabase enterprise SSO, role-based access control, and organization management.
5
- Use when implementing SSO integration, configuring role-based permissions,
6
- or setting up organization-level controls for Supabase.
7
- Trigger with phrases like "supabase SSO", "supabase RBAC",
8
- "supabase enterprise", "supabase roles", "supabase permissions", "supabase SAML".
9
- allowed-tools: Read, Write, Edit
3
+ description: 'Implement custom role-based access control via JWT claims in Supabase:
4
+ app_metadata.role,
5
+
6
+ RLS policies with auth.jwt() role extraction, organization-scoped access, and API
7
+ key scoping.
8
+
9
+ Use when implementing role-based permissions, configuring organization-level access,
10
+
11
+ building admin/member/viewer hierarchies, or scoping API keys per role.
12
+
13
+ Trigger: "supabase RBAC", "supabase roles", "supabase permissions", "supabase JWT
14
+ claims",
15
+
16
+ "supabase organization access", "supabase custom roles", "supabase app_metadata".
17
+
18
+ '
19
+ allowed-tools: Read, Write, Edit, Bash(npx supabase:*), Bash(supabase:*), Bash(psql:*),
20
+ Grep, Glob
10
21
  version: 1.0.0
11
22
  license: MIT
12
23
  author: Jeremy Longshore <jeremy@intentsolutions.io>
24
+ tags:
25
+ - saas
26
+ - supabase
27
+ - rbac
28
+ - security
29
+ - enterprise
30
+ - roles
31
+ - permissions
32
+ compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
13
33
  ---
14
-
15
34
  # Supabase Enterprise RBAC
16
35
 
17
36
  ## Overview
18
- Configure enterprise-grade access control for Supabase integrations.
19
37
 
20
- ## Prerequisites
21
- - Supabase Enterprise tier subscription
22
- - Identity Provider (IdP) with SAML/OIDC support
23
- - Understanding of role-based access patterns
24
- - Audit logging infrastructure
38
+ Supabase supports custom role-based access control (RBAC) by storing role information in `app_metadata` on the user's JWT, then reading those claims in RLS policies via `auth.jwt() ->> 'role'`. This skill implements a complete RBAC system: defining roles in `app_metadata`, writing RLS policies that enforce role hierarchies, scoping access by organization, managing roles through the Admin API, and protecting API endpoints with role checks — all using real `createClient` from `@supabase/supabase-js`.
25
39
 
26
- ## Role Definitions
40
+ **When to use:** Building multi-role applications (admin/editor/viewer), implementing organization-scoped access, creating custom permission systems beyond Supabase's built-in `anon`/`authenticated` roles, or scoping API operations by user role.
27
41
 
28
- | Role | Permissions | Use Case |
29
- |------|-------------|----------|
30
- | Admin | Full access | Platform administrators |
31
- | Developer | Read/write, no delete | Active development |
32
- | Viewer | Read-only | Stakeholders, auditors |
33
- | Service | API access only | Automated systems |
42
+ ## Prerequisites
34
43
 
35
- ## Role Implementation
44
+ - `@supabase/supabase-js` v2+ with service role key for admin operations
45
+ - Understanding of JWT claims and Supabase's `auth.jwt()` SQL function
46
+ - Database access via SQL Editor or `psql` for RLS policy creation
47
+ - Supabase project with authentication configured
36
48
 
37
- ```typescript
38
- enum SupabaseRole {
39
- Admin = 'admin',
40
- Developer = 'developer',
41
- Viewer = 'viewer',
42
- Service = 'service',
43
- }
49
+ ## Instructions
44
50
 
45
- interface SupabasePermissions {
46
- read: boolean;
47
- write: boolean;
48
- delete: boolean;
49
- admin: boolean;
50
- }
51
+ ### Step 1: Define Roles via app_metadata and JWT Claims
51
52
 
52
- const ROLE_PERMISSIONS: Record<SupabaseRole, SupabasePermissions> = {
53
- admin: { read: true, write: true, delete: true, admin: true },
54
- developer: { read: true, write: true, delete: false, admin: false },
55
- viewer: { read: true, write: false, delete: false, admin: false },
56
- service: { read: true, write: true, delete: false, admin: false },
57
- };
58
-
59
- function checkPermission(
60
- role: SupabaseRole,
61
- action: keyof SupabasePermissions
62
- ): boolean {
63
- return ROLE_PERMISSIONS[role][action];
64
- }
65
- ```
53
+ Store custom roles in the user's `app_metadata` using the Admin API. These claims appear in every JWT the user receives and are available in RLS policies.
66
54
 
67
- ## SSO Integration
68
-
69
- ### SAML Configuration
55
+ **Set user roles with the Admin API:**
70
56
 
71
57
  ```typescript
72
- // Supabase SAML setup
73
- const samlConfig = {
74
- entryPoint: 'https://idp.company.com/saml/sso',
75
- issuer: 'https://supabase.com/saml/metadata',
76
- cert: process.env.SAML_CERT,
77
- callbackUrl: 'https://app.yourcompany.com/auth/supabase/callback',
78
- };
79
-
80
- // Map IdP groups to Supabase roles
81
- const groupRoleMapping: Record<string, SupabaseRole> = {
82
- 'Engineering': SupabaseRole.Developer,
83
- 'Platform-Admins': SupabaseRole.Admin,
84
- 'Data-Team': SupabaseRole.Viewer,
85
- };
86
- ```
58
+ import { createClient } from '@supabase/supabase-js';
87
59
 
88
- ### OAuth2/OIDC Integration
89
-
90
- ```typescript
91
- import { OAuth2Client } from '@supabase/supabase-js';
92
-
93
- const oauthClient = new OAuth2Client({
94
- clientId: process.env.SUPABASE_OAUTH_CLIENT_ID!,
95
- clientSecret: process.env.SUPABASE_OAUTH_CLIENT_SECRET!,
96
- redirectUri: 'https://app.yourcompany.com/auth/supabase/callback',
97
- scopes: read, write, realtime,
98
- });
99
- ```
60
+ const supabase = createClient(
61
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
62
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
63
+ { auth: { autoRefreshToken: false, persistSession: false } }
64
+ );
100
65
 
101
- ## Organization Management
66
+ // Define the role hierarchy
67
+ type AppRole = 'admin' | 'editor' | 'viewer' | 'member';
102
68
 
103
- ```typescript
104
- interface SupabaseOrganization {
105
- id: string;
106
- name: string;
107
- ssoEnabled: boolean;
108
- enforceSso: boolean;
109
- allowedDomains: string[];
110
- defaultRole: SupabaseRole;
69
+ interface AppMetadata {
70
+ role: AppRole;
71
+ org_id: string;
72
+ permissions?: string[];
111
73
  }
112
74
 
113
- async function createOrganization(
114
- config: SupabaseOrganization
115
- ): Promise<void> {
116
- await supabaseClient.organizations.create({
117
- ...config,
118
- settings: {
119
- sso: {
120
- enabled: config.ssoEnabled,
121
- enforced: config.enforceSso,
122
- domains: config.allowedDomains,
123
- },
75
+ // Assign a role to a user (admin operation)
76
+ async function setUserRole(userId: string, role: AppRole, orgId: string) {
77
+ const { data, error } = await supabase.auth.admin.updateUserById(userId, {
78
+ app_metadata: {
79
+ role,
80
+ org_id: orgId,
124
81
  },
125
82
  });
126
- }
127
- ```
128
83
 
129
- ## Access Control Middleware
84
+ if (error) throw new Error(`Failed to set role: ${error.message}`);
130
85
 
131
- ```typescript
132
- function requireSupabasePermission(
133
- requiredPermission: keyof SupabasePermissions
134
- ) {
135
- return async (req: Request, res: Response, next: NextFunction) => {
136
- const user = req.user as { supabaseRole: SupabaseRole };
86
+ console.log(`User ${userId} assigned role "${role}" in org "${orgId}"`);
87
+ return data.user;
88
+ }
137
89
 
138
- if (!checkPermission(user.supabaseRole, requiredPermission)) {
139
- return res.status(403).json({
140
- error: 'Forbidden',
141
- message: `Missing permission: ${requiredPermission}`,
142
- });
143
- }
90
+ // Assign granular permissions (optional, for fine-grained control)
91
+ async function setUserPermissions(
92
+ userId: string,
93
+ permissions: string[]
94
+ ) {
95
+ const { data, error } = await supabase.auth.admin.updateUserById(userId, {
96
+ app_metadata: { permissions },
97
+ });
144
98
 
145
- next();
146
- };
99
+ if (error) throw new Error(`Failed to set permissions: ${error.message}`);
100
+ return data.user;
147
101
  }
148
102
 
149
- // Usage
150
- app.delete('/supabase/resource/:id',
151
- requireSupabasePermission('delete'),
152
- deleteResourceHandler
153
- );
103
+ // Bulk role assignment (e.g., onboarding a team)
104
+ async function assignTeamRoles(
105
+ orgId: string,
106
+ assignments: { userId: string; role: AppRole }[]
107
+ ) {
108
+ const results = await Promise.allSettled(
109
+ assignments.map(({ userId, role }) => setUserRole(userId, role, orgId))
110
+ );
111
+
112
+ const succeeded = results.filter((r) => r.status === 'fulfilled').length;
113
+ const failed = results.filter((r) => r.status === 'rejected').length;
114
+ console.log(`Assigned ${succeeded} roles, ${failed} failures`);
115
+ }
154
116
  ```
155
117
 
156
- ## Audit Trail
118
+ **Read roles from the JWT in application code:**
157
119
 
158
120
  ```typescript
159
- interface SupabaseAuditEntry {
160
- timestamp: Date;
161
- userId: string;
162
- role: SupabaseRole;
163
- action: string;
164
- resource: string;
165
- success: boolean;
166
- ipAddress: string;
121
+ import { createClient } from '@supabase/supabase-js';
122
+
123
+ const supabase = createClient(
124
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
125
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
126
+ );
127
+
128
+ // Get the current user's role from their JWT
129
+ async function getCurrentUserRole(): Promise<AppRole | null> {
130
+ const { data: { user }, error } = await supabase.auth.getUser();
131
+ if (error || !user) return null;
132
+
133
+ return (user.app_metadata?.role as AppRole) ?? null;
134
+ }
135
+
136
+ // Get the current user's organization
137
+ async function getCurrentOrg(): Promise<string | null> {
138
+ const { data: { user } } = await supabase.auth.getUser();
139
+ return user?.app_metadata?.org_id ?? null;
167
140
  }
168
141
 
169
- async function logSupabaseAccess(entry: SupabaseAuditEntry): Promise<void> {
170
- await auditDb.insert(entry);
142
+ // Check if current user has a specific role or higher
143
+ function hasRole(userRole: AppRole, requiredRole: AppRole): boolean {
144
+ const hierarchy: Record<AppRole, number> = {
145
+ admin: 4,
146
+ editor: 3,
147
+ member: 2,
148
+ viewer: 1,
149
+ };
150
+ return hierarchy[userRole] >= hierarchy[requiredRole];
151
+ }
171
152
 
172
- // Alert on suspicious activity
173
- if (entry.action === 'delete' && !entry.success) {
174
- await alertOnSuspiciousActivity(entry);
153
+ // Middleware-style role check for API routes
154
+ async function requireRole(requiredRole: AppRole) {
155
+ const role = await getCurrentUserRole();
156
+ if (!role || !hasRole(role, requiredRole)) {
157
+ throw new Error(
158
+ `Access denied: requires "${requiredRole}" role, user has "${role ?? 'none'}"`
159
+ );
175
160
  }
176
161
  }
177
162
  ```
178
163
 
179
- ## Instructions
164
+ ### Step 2: RLS Policies with JWT Role Claims
165
+
166
+ Write Row Level Security policies that read `auth.jwt() ->> 'role'` and `auth.jwt() -> 'app_metadata' ->> 'org_id'` to enforce role-based and organization-scoped access.
167
+
168
+ **Role-based RLS policies:**
169
+
170
+ ```sql
171
+ -- Create a helper function to extract role from JWT
172
+ CREATE OR REPLACE FUNCTION public.get_user_role()
173
+ RETURNS text AS $$
174
+ SELECT coalesce(
175
+ auth.jwt() -> 'app_metadata' ->> 'role',
176
+ 'viewer' -- default role if not set
177
+ );
178
+ $$ LANGUAGE sql STABLE SECURITY DEFINER;
179
+
180
+ -- Create a helper function to extract org_id from JWT
181
+ CREATE OR REPLACE FUNCTION public.get_user_org_id()
182
+ RETURNS text AS $$
183
+ SELECT auth.jwt() -> 'app_metadata' ->> 'org_id';
184
+ $$ LANGUAGE sql STABLE SECURITY DEFINER;
185
+
186
+ -- Enable RLS on all tables
187
+ ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
188
+ ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
189
+ ALTER TABLE public.team_members ENABLE ROW LEVEL SECURITY;
190
+
191
+ -- Projects: org members can read, editors+ can create/update, admins can delete
192
+ CREATE POLICY "org_members_read_projects" ON public.projects
193
+ FOR SELECT USING (
194
+ org_id = get_user_org_id()
195
+ );
196
+
197
+ CREATE POLICY "editors_create_projects" ON public.projects
198
+ FOR INSERT WITH CHECK (
199
+ org_id = get_user_org_id()
200
+ AND get_user_role() IN ('admin', 'editor')
201
+ );
202
+
203
+ CREATE POLICY "editors_update_projects" ON public.projects
204
+ FOR UPDATE USING (
205
+ org_id = get_user_org_id()
206
+ AND get_user_role() IN ('admin', 'editor')
207
+ );
208
+
209
+ CREATE POLICY "admins_delete_projects" ON public.projects
210
+ FOR DELETE USING (
211
+ org_id = get_user_org_id()
212
+ AND get_user_role() = 'admin'
213
+ );
214
+
215
+ -- Documents: org-scoped with role-based write access
216
+ CREATE POLICY "org_read_documents" ON public.documents
217
+ FOR SELECT USING (
218
+ org_id = get_user_org_id()
219
+ );
220
+
221
+ CREATE POLICY "editors_write_documents" ON public.documents
222
+ FOR INSERT WITH CHECK (
223
+ org_id = get_user_org_id()
224
+ AND get_user_role() IN ('admin', 'editor')
225
+ );
226
+
227
+ CREATE POLICY "owner_or_admin_update_documents" ON public.documents
228
+ FOR UPDATE USING (
229
+ org_id = get_user_org_id()
230
+ AND (
231
+ created_by = auth.uid()
232
+ OR get_user_role() = 'admin'
233
+ )
234
+ );
235
+
236
+ -- Team members: admins manage team, members can read
237
+ CREATE POLICY "org_read_team" ON public.team_members
238
+ FOR SELECT USING (
239
+ org_id = get_user_org_id()
240
+ );
241
+
242
+ CREATE POLICY "admins_manage_team" ON public.team_members
243
+ FOR ALL USING (
244
+ org_id = get_user_org_id()
245
+ AND get_user_role() = 'admin'
246
+ );
247
+ ```
248
+
249
+ **Organization-scoped access table schema:**
250
+
251
+ ```sql
252
+ -- Organizations table
253
+ CREATE TABLE public.organizations (
254
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
255
+ name text NOT NULL,
256
+ slug text UNIQUE NOT NULL,
257
+ created_at timestamptz DEFAULT now()
258
+ );
180
259
 
181
- ### Step 1: Define Roles
182
- Map organizational roles to Supabase permissions.
260
+ -- Team members junction table
261
+ CREATE TABLE public.team_members (
262
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
263
+ org_id uuid REFERENCES public.organizations(id) ON DELETE CASCADE,
264
+ user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
265
+ role text NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'editor', 'member', 'viewer')),
266
+ invited_by uuid REFERENCES auth.users(id),
267
+ created_at timestamptz DEFAULT now(),
268
+ UNIQUE(org_id, user_id)
269
+ );
183
270
 
184
- ### Step 2: Configure SSO
185
- Set up SAML or OIDC integration with your IdP.
271
+ -- Projects scoped to organizations
272
+ CREATE TABLE public.projects (
273
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
274
+ org_id uuid REFERENCES public.organizations(id) ON DELETE CASCADE,
275
+ name text NOT NULL,
276
+ created_by uuid REFERENCES auth.users(id),
277
+ created_at timestamptz DEFAULT now()
278
+ );
279
+
280
+ -- Index for fast org-scoped queries
281
+ CREATE INDEX idx_team_members_org ON public.team_members(org_id);
282
+ CREATE INDEX idx_team_members_user ON public.team_members(user_id);
283
+ CREATE INDEX idx_projects_org ON public.projects(org_id);
284
+ ```
186
285
 
187
- ### Step 3: Implement Middleware
188
- Add permission checks to API endpoints.
286
+ ### Step 3: API Key Scoping and Role Enforcement in Application Code
189
287
 
190
- ### Step 4: Enable Audit Logging
191
- Track all access for compliance.
288
+ See [API key scoping and role enforcement](references/api-scoping-and-enforcement.md) for server-side `withRole()` middleware, per-request client creation, admin panel operations (list members, invite users, change roles), and organization management patterns.
192
289
 
193
290
  ## Output
194
- - Role definitions implemented
195
- - SSO integration configured
196
- - Permission middleware active
197
- - Audit trail enabled
291
+
292
+ After completing this skill, you will have:
293
+
294
+ - **Role assignment via app_metadata** — `admin.updateUserById()` sets role claims on user JWTs
295
+ - **JWT claim extraction** — `get_user_role()` and `get_user_org_id()` SQL helper functions
296
+ - **Role-based RLS policies** — SELECT/INSERT/UPDATE/DELETE scoped by role hierarchy (admin > editor > member > viewer)
297
+ - **Organization-scoped access** — multi-tenant isolation via `org_id` in JWT claims and RLS policies
298
+ - **Application-layer enforcement** — `withRole()` middleware for API routes with proper 401/403 responses
299
+ - **Admin panel operations** — list members, invite users, change roles with both database and JWT updates
300
+ - **Role hierarchy checking** — `hasRole()` function supporting role escalation comparison
198
301
 
199
302
  ## Error Handling
200
- | Issue | Cause | Solution |
303
+
304
+ | Error | Cause | Solution |
201
305
  |-------|-------|----------|
202
- | SSO login fails | Wrong callback URL | Verify IdP config |
203
- | Permission denied | Missing role mapping | Update group mappings |
204
- | Token expired | Short TTL | Refresh token logic |
205
- | Audit gaps | Async logging failed | Check log pipeline |
306
+ | `app_metadata.role` is null in JWT | Role not set or user needs to re-login | Call `admin.updateUserById()` to set role; user must refresh their session |
307
+ | RLS policy returns empty results | JWT claims don't match policy conditions | Check `auth.jwt()` output in SQL Editor; verify `app_metadata` was set correctly |
308
+ | `permission denied for function` | Helper function not created or wrong schema | Create `get_user_role()` in the `public` schema with `SECURITY DEFINER` |
309
+ | User role changes not reflected | JWT cached with old claims | User must sign out and sign in again, or call `supabase.auth.refreshSession()` |
310
+ | `duplicate key value violates unique constraint` | User already in organization | Check `team_members` table for existing entry before inserting |
311
+ | `foreign key violation` on team_members | User or org doesn't exist | Verify both `user_id` and `org_id` exist before inserting membership |
312
+ | Role hierarchy bypass | Direct database access with service role | Service role bypasses RLS by design — restrict its use to server-side admin operations only |
206
313
 
207
314
  ## Examples
208
315
 
209
- ### Quick Permission Check
316
+ **Example 1 — Quick role check in a component:**
317
+
210
318
  ```typescript
211
- if (!checkPermission(user.role, 'write')) {
212
- throw new ForbiddenError('Write permission required');
319
+ import { createClient } from '@supabase/supabase-js';
320
+
321
+ const supabase = createClient(url, anonKey);
322
+
323
+ async function canEditProject(): Promise<boolean> {
324
+ const { data: { user } } = await supabase.auth.getUser();
325
+ const role = user?.app_metadata?.role;
326
+ return role === 'admin' || role === 'editor';
327
+ }
328
+ ```
329
+
330
+ **Example 2 — Verify RLS policies work correctly:**
331
+
332
+ ```sql
333
+ -- Test as an editor in org-123
334
+ SET request.jwt.claims = '{"sub": "user-uuid", "role": "authenticated", "app_metadata": {"role": "editor", "org_id": "org-123"}}';
335
+
336
+ -- Should return only org-123 projects
337
+ SELECT * FROM projects;
338
+
339
+ -- Should succeed (editors can create)
340
+ INSERT INTO projects (org_id, name, created_by) VALUES ('org-123', 'Test', 'user-uuid');
341
+
342
+ -- Should fail (editors cannot delete)
343
+ DELETE FROM projects WHERE id = 'some-project-id';
344
+
345
+ RESET request.jwt.claims;
346
+ ```
347
+
348
+ **Example 3 — Onboard a new organization:**
349
+
350
+ ```typescript
351
+ async function onboardOrganization(orgName: string, adminEmail: string) {
352
+ // 1. Create the organization
353
+ const { data: org } = await adminClient
354
+ .from('organizations')
355
+ .insert({ name: orgName, slug: orgName.toLowerCase().replace(/\s+/g, '-') })
356
+ .select('id')
357
+ .single();
358
+
359
+ // 2. Assign the creator as admin
360
+ const { data: { users } } = await adminClient.auth.admin.listUsers();
361
+ const adminUser = users.find((u) => u.email === adminEmail);
362
+
363
+ if (adminUser && org) {
364
+ await setUserRole(adminUser.id, 'admin', org.id);
365
+ await adminClient.from('team_members').insert({
366
+ org_id: org.id,
367
+ user_id: adminUser.id,
368
+ role: 'admin',
369
+ });
370
+ }
371
+
372
+ return org;
213
373
  }
214
374
  ```
215
375
 
216
376
  ## Resources
217
- - [Supabase Enterprise Guide](https://supabase.com/docs/enterprise)
218
- - [SAML 2.0 Specification](https://wiki.oasis-open.org/security/FrontPage)
219
- - [OpenID Connect Spec](https://openid.net/specs/openid-connect-core-1_0.html)
377
+
378
+ - [Custom Claims and RBAC — Supabase Docs](https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac)
379
+ - [Auth Admin updateUserById — Supabase Docs](https://supabase.com/docs/reference/javascript/auth-admin-updateuserbyid)
380
+ - [Row Level Security — Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security)
381
+ - [auth.jwt() Function — Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security#helper-functions)
382
+ - [Multi-tenancy Patterns — Supabase Docs](https://supabase.com/docs/guides/getting-started/architecture#multi-tenancy)
383
+ - [inviteUserByEmail — Supabase Docs](https://supabase.com/docs/reference/javascript/auth-admin-inviteuserbyemail)
220
384
 
221
385
  ## Next Steps
222
- For major migrations, see `supabase-migration-deep-dive`.
386
+
387
+ - For database migration patterns, see `supabase-migration-deep-dive`
388
+ - For security hardening and API key scoping, see `supabase-security-basics`
389
+ - For data handling and GDPR compliance, see `supabase-data-handling`