@intentsolutionsio/supabase-pack 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +73 -47
- package/package.json +4 -4
- package/skills/supabase-advanced-troubleshooting/SKILL.md +404 -200
- package/skills/supabase-advanced-troubleshooting/references/errors.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/evidence-collection-framework.md +34 -0
- package/skills/supabase-advanced-troubleshooting/references/examples.md +11 -0
- package/skills/supabase-advanced-troubleshooting/references/rls-edge-functions-realtime.md +363 -0
- package/skills/supabase-advanced-troubleshooting/references/systematic-isolation.md +56 -0
- package/skills/supabase-advanced-troubleshooting/references/timing-analysis.md +35 -0
- package/skills/supabase-architecture-variants/SKILL.md +395 -216
- package/skills/supabase-architecture-variants/references/errors.md +11 -0
- package/skills/supabase-architecture-variants/references/examples.md +12 -0
- package/skills/supabase-architecture-variants/references/serverless-and-multi-tenant.md +251 -0
- package/skills/supabase-architecture-variants/references/variant-a-monolith-(simple).md +44 -0
- package/skills/supabase-architecture-variants/references/variant-b-service-layer-(moderate).md +72 -0
- package/skills/supabase-architecture-variants/references/variant-c-microservice-(complex).md +81 -0
- package/skills/supabase-auth-storage-realtime-core/SKILL.md +471 -37
- package/skills/supabase-ci-integration/SKILL.md +315 -67
- package/skills/supabase-ci-integration/references/errors.md +10 -0
- package/skills/supabase-ci-integration/references/examples.md +36 -0
- package/skills/supabase-ci-integration/references/implementation.md +54 -0
- package/skills/supabase-common-errors/SKILL.md +320 -62
- package/skills/supabase-common-errors/references/errors.md +53 -0
- package/skills/supabase-common-errors/references/examples.md +23 -0
- package/skills/supabase-cost-tuning/SKILL.md +365 -131
- package/skills/supabase-cost-tuning/references/cost-estimation.md +34 -0
- package/skills/supabase-cost-tuning/references/cost-reduction-strategies.md +40 -0
- package/skills/supabase-cost-tuning/references/errors.md +11 -0
- package/skills/supabase-cost-tuning/references/examples.md +15 -0
- package/skills/supabase-data-handling/SKILL.md +378 -145
- package/skills/supabase-data-handling/references/errors.md +11 -0
- package/skills/supabase-data-handling/references/examples.md +27 -0
- package/skills/supabase-data-handling/references/implementation.md +223 -0
- package/skills/supabase-data-handling/references/retention-and-backup.md +221 -0
- package/skills/supabase-debug-bundle/SKILL.md +267 -73
- package/skills/supabase-debug-bundle/references/errors.md +12 -0
- package/skills/supabase-debug-bundle/references/examples.md +24 -0
- package/skills/supabase-debug-bundle/references/implementation.md +54 -0
- package/skills/supabase-deploy-integration/SKILL.md +258 -147
- package/skills/supabase-deploy-integration/references/errors.md +11 -0
- package/skills/supabase-deploy-integration/references/examples.md +21 -0
- package/skills/supabase-deploy-integration/references/google-cloud-run.md +36 -0
- package/skills/supabase-deploy-integration/references/vercel-deployment.md +35 -0
- package/skills/supabase-enterprise-rbac/SKILL.md +327 -160
- package/skills/supabase-enterprise-rbac/references/api-scoping-and-enforcement.md +255 -0
- package/skills/supabase-enterprise-rbac/references/errors.md +11 -0
- package/skills/supabase-enterprise-rbac/references/examples.md +12 -0
- package/skills/supabase-enterprise-rbac/references/role-implementation.md +33 -0
- package/skills/supabase-enterprise-rbac/references/sso-integration.md +35 -0
- package/skills/supabase-hello-world/SKILL.md +160 -54
- package/skills/supabase-incident-runbook/SKILL.md +453 -131
- package/skills/supabase-incident-runbook/references/errors.md +11 -0
- package/skills/supabase-incident-runbook/references/examples.md +10 -0
- package/skills/supabase-incident-runbook/references/immediate-actions-by-error-type.md +41 -0
- package/skills/supabase-install-auth/SKILL.md +186 -50
- package/skills/supabase-install-auth/references/examples.md +102 -0
- package/skills/supabase-known-pitfalls/SKILL.md +411 -241
- package/skills/supabase-known-pitfalls/references/errors.md +11 -0
- package/skills/supabase-known-pitfalls/references/examples.md +12 -0
- package/skills/supabase-load-scale/SKILL.md +346 -217
- package/skills/supabase-load-scale/references/capacity-planning.md +47 -0
- package/skills/supabase-load-scale/references/errors.md +11 -0
- package/skills/supabase-load-scale/references/examples.md +26 -0
- package/skills/supabase-load-scale/references/load-testing-with-k6.md +59 -0
- package/skills/supabase-load-scale/references/scaling-patterns.md +65 -0
- package/skills/supabase-load-scale/references/table-partitioning.md +263 -0
- package/skills/supabase-local-dev-loop/SKILL.md +272 -73
- package/skills/supabase-local-dev-loop/references/errors.md +11 -0
- package/skills/supabase-local-dev-loop/references/examples.md +21 -0
- package/skills/supabase-local-dev-loop/references/implementation.md +60 -0
- package/skills/supabase-migration-deep-dive/SKILL.md +338 -177
- package/skills/supabase-migration-deep-dive/references/backfill-versioning-rollback.md +258 -0
- package/skills/supabase-migration-deep-dive/references/errors.md +11 -0
- package/skills/supabase-migration-deep-dive/references/examples.md +12 -0
- package/skills/supabase-migration-deep-dive/references/implementation-plan.md +80 -0
- package/skills/supabase-migration-deep-dive/references/pre-migration-assessment.md +39 -0
- package/skills/supabase-multi-env-setup/SKILL.md +393 -152
- package/skills/supabase-multi-env-setup/references/configuration-structure.md +59 -0
- package/skills/supabase-multi-env-setup/references/errors.md +11 -0
- package/skills/supabase-multi-env-setup/references/examples.md +11 -0
- package/skills/supabase-observability/SKILL.md +318 -196
- package/skills/supabase-observability/references/alert-configuration.md +40 -0
- package/skills/supabase-observability/references/errors.md +11 -0
- package/skills/supabase-observability/references/examples.md +13 -0
- package/skills/supabase-observability/references/metrics-collection.md +65 -0
- package/skills/supabase-performance-tuning/SKILL.md +304 -160
- package/skills/supabase-performance-tuning/references/caching-strategy.md +49 -0
- package/skills/supabase-performance-tuning/references/errors.md +11 -0
- package/skills/supabase-performance-tuning/references/examples.md +13 -0
- package/skills/supabase-policy-guardrails/SKILL.md +248 -221
- package/skills/supabase-policy-guardrails/references/ci-cost-security.md +484 -0
- package/skills/supabase-policy-guardrails/references/errors.md +11 -0
- package/skills/supabase-policy-guardrails/references/eslint-rules.md +46 -0
- package/skills/supabase-policy-guardrails/references/examples.md +10 -0
- package/skills/supabase-prod-checklist/SKILL.md +474 -84
- package/skills/supabase-prod-checklist/references/errors.md +63 -0
- package/skills/supabase-prod-checklist/references/examples.md +153 -0
- package/skills/supabase-prod-checklist/references/implementation.md +113 -0
- package/skills/supabase-rate-limits/SKILL.md +311 -98
- package/skills/supabase-rate-limits/references/errors.md +11 -0
- package/skills/supabase-rate-limits/references/examples.md +46 -0
- package/skills/supabase-rate-limits/references/implementation.md +66 -0
- package/skills/supabase-reference-architecture/SKILL.md +249 -182
- package/skills/supabase-reference-architecture/references/errors.md +29 -0
- package/skills/supabase-reference-architecture/references/examples.md +116 -0
- package/skills/supabase-reference-architecture/references/key-components.md +244 -0
- package/skills/supabase-reference-architecture/references/project-structure.md +109 -0
- package/skills/supabase-reliability-patterns/SKILL.md +229 -234
- package/skills/supabase-reliability-patterns/references/circuit-breaker.md +36 -0
- package/skills/supabase-reliability-patterns/references/dead-letter-queue.md +48 -0
- package/skills/supabase-reliability-patterns/references/errors.md +11 -0
- package/skills/supabase-reliability-patterns/references/examples.md +11 -0
- package/skills/supabase-reliability-patterns/references/idempotency-keys.md +36 -0
- package/skills/supabase-reliability-patterns/references/offline-degradation-health-dualwrite.md +489 -0
- package/skills/supabase-schema-from-requirements/SKILL.md +373 -34
- package/skills/supabase-sdk-patterns/SKILL.md +388 -99
- package/skills/supabase-sdk-patterns/references/errors.md +11 -0
- package/skills/supabase-sdk-patterns/references/examples.md +45 -0
- package/skills/supabase-sdk-patterns/references/implementation.md +67 -0
- package/skills/supabase-security-basics/SKILL.md +282 -102
- package/skills/supabase-security-basics/references/errors.md +10 -0
- package/skills/supabase-security-basics/references/examples.md +70 -0
- package/skills/supabase-security-basics/references/implementation.md +39 -0
- package/skills/supabase-upgrade-migration/SKILL.md +248 -66
- package/skills/supabase-upgrade-migration/references/errors.md +10 -0
- package/skills/supabase-upgrade-migration/references/examples.md +51 -0
- package/skills/supabase-upgrade-migration/references/implementation.md +29 -0
- package/skills/supabase-webhooks-events/SKILL.md +412 -138
- package/skills/supabase-webhooks-events/references/errors.md +55 -0
- package/skills/supabase-webhooks-events/references/event-handler-pattern.md +106 -0
- package/skills/supabase-webhooks-events/references/examples.md +133 -0
- package/skills/supabase-webhooks-events/references/signature-verification.md +165 -0
|
@@ -1,238 +1,305 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: supabase-reference-architecture
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
description: "Implement enterprise Supabase reference architectures \u2014 monorepo\
|
|
4
|
+
\ layout, multi-tenant RLS,\nmicroservices with cross-project access, framework\
|
|
5
|
+
\ integration, edge functions, caching,\nqueue patterns, and audit logging.\nUse\
|
|
6
|
+
\ when designing a new Supabase project from scratch, reviewing project structure\
|
|
7
|
+
\ for\nproduction readiness, planning multi-tenant isolation, or establishing team\
|
|
8
|
+
\ architecture standards.\nTrigger with phrases like \"supabase architecture\",\
|
|
9
|
+
\ \"supabase project structure\",\n\"supabase monorepo\", \"supabase multi-tenant\"\
|
|
10
|
+
, \"supabase reference design\",\n\"how to organize supabase at scale\".\n"
|
|
11
|
+
allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(npx:*), Bash(supabase:*), Grep,
|
|
12
|
+
Glob
|
|
10
13
|
version: 1.0.0
|
|
11
14
|
license: MIT
|
|
12
15
|
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
16
|
+
tags:
|
|
17
|
+
- saas
|
|
18
|
+
- supabase
|
|
19
|
+
- architecture
|
|
20
|
+
- patterns
|
|
21
|
+
- multi-tenant
|
|
22
|
+
- monorepo
|
|
23
|
+
compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
|
|
13
24
|
---
|
|
14
|
-
|
|
15
25
|
# Supabase Reference Architecture
|
|
16
26
|
|
|
17
27
|
## Overview
|
|
18
|
-
Production-ready architecture patterns for Supabase integrations.
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
- Understanding of layered architecture
|
|
22
|
-
- Supabase SDK knowledge
|
|
23
|
-
- TypeScript project setup
|
|
24
|
-
- Testing framework configured
|
|
29
|
+
Production Supabase applications need more than a flat `lib/supabase.ts` file. This skill covers five enterprise architecture patterns: monorepo with shared types, multi-tenant RLS isolation, microservices with separate Supabase projects, framework integration (Next.js / SvelteKit), and operational patterns (edge functions, caching, queues, audit trails). Each pattern stands alone — pick the ones that match your scale.
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
For the full monorepo directory layout and microservices cross-project access, see [Project Structure](references/project-structure.md). For edge functions, caching, queue, and audit trail patterns, see [Operational Patterns](references/key-components.md).
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
my-supabase-project/
|
|
30
|
-
├── src/
|
|
31
|
-
│ ├── supabase/
|
|
32
|
-
│ │ ├── client.ts # Singleton client wrapper
|
|
33
|
-
│ │ ├── config.ts # Environment configuration
|
|
34
|
-
│ │ ├── types.ts # TypeScript types
|
|
35
|
-
│ │ ├── errors.ts # Custom error classes
|
|
36
|
-
│ │ └── handlers/
|
|
37
|
-
│ │ ├── webhooks.ts # Webhook handlers
|
|
38
|
-
│ │ └── events.ts # Event processing
|
|
39
|
-
│ ├── services/
|
|
40
|
-
│ │ └── supabase/
|
|
41
|
-
│ │ ├── index.ts # Service facade
|
|
42
|
-
│ │ ├── sync.ts # Data synchronization
|
|
43
|
-
│ │ └── cache.ts # Caching layer
|
|
44
|
-
│ ├── api/
|
|
45
|
-
│ │ └── supabase/
|
|
46
|
-
│ │ └── webhook.ts # Webhook endpoint
|
|
47
|
-
│ └── jobs/
|
|
48
|
-
│ └── supabase/
|
|
49
|
-
│ └── sync.ts # Background sync job
|
|
50
|
-
├── tests/
|
|
51
|
-
│ ├── unit/
|
|
52
|
-
│ │ └── supabase/
|
|
53
|
-
│ └── integration/
|
|
54
|
-
│ └── supabase/
|
|
55
|
-
├── config/
|
|
56
|
-
│ ├── supabase.development.json
|
|
57
|
-
│ ├── supabase.staging.json
|
|
58
|
-
│ └── supabase.production.json
|
|
59
|
-
└── docs/
|
|
60
|
-
└── supabase/
|
|
61
|
-
├── SETUP.md
|
|
62
|
-
└── RUNBOOK.md
|
|
63
|
-
```
|
|
33
|
+
## Prerequisites
|
|
64
34
|
|
|
65
|
-
|
|
35
|
+
- `@supabase/supabase-js` v2+ installed (`npm install @supabase/supabase-js`)
|
|
36
|
+
- Supabase CLI installed (`npm install -g supabase`)
|
|
37
|
+
- A Supabase project at [supabase.com/dashboard](https://supabase.com/dashboard)
|
|
38
|
+
- Familiarity with `supabase-install-auth` (project URL, anon key, service role key)
|
|
39
|
+
- PostgreSQL basics (RLS policies, triggers, functions)
|
|
66
40
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
│ (Controllers, Routes, Webhooks) │
|
|
71
|
-
├─────────────────────────────────────────┤
|
|
72
|
-
│ Service Layer │
|
|
73
|
-
│ (Business Logic, Orchestration) │
|
|
74
|
-
├─────────────────────────────────────────┤
|
|
75
|
-
│ Supabase Layer │
|
|
76
|
-
│ (Client, Types, Error Handling) │
|
|
77
|
-
├─────────────────────────────────────────┤
|
|
78
|
-
│ Infrastructure Layer │
|
|
79
|
-
│ (Cache, Queue, Monitoring) │
|
|
80
|
-
└─────────────────────────────────────────┘
|
|
81
|
-
```
|
|
41
|
+
## Instructions
|
|
42
|
+
|
|
43
|
+
### Step 1: Client Singleton — The Foundation
|
|
82
44
|
|
|
83
|
-
|
|
45
|
+
Every app in the monorepo imports from a shared package instead of creating its own client. This guarantees a single source of truth for the URL, keys, and type definitions.
|
|
84
46
|
|
|
85
|
-
### Step 1: Client Wrapper
|
|
86
47
|
```typescript
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
private cache: Cache;
|
|
91
|
-
private monitor: Monitor;
|
|
92
|
-
|
|
93
|
-
constructor(config: SupabaseConfig) {
|
|
94
|
-
this.client = new SupabaseClient(config);
|
|
95
|
-
this.cache = new Cache(config.cacheOptions);
|
|
96
|
-
this.monitor = new Monitor('supabase');
|
|
97
|
-
}
|
|
48
|
+
// packages/supabase/src/client.ts
|
|
49
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
|
50
|
+
import type { Database } from './database.types'
|
|
98
51
|
|
|
99
|
-
|
|
100
|
-
return this.cache.getOrFetch(id, () =>
|
|
101
|
-
this.monitor.track('get', () => this.client.get(id))
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
52
|
+
let client: SupabaseClient<Database> | null = null
|
|
106
53
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
public readonly originalError?: Error
|
|
116
|
-
) {
|
|
117
|
-
super(message);
|
|
118
|
-
this.name = 'SupabaseServiceError';
|
|
54
|
+
export function getSupabaseClient(): SupabaseClient<Database> {
|
|
55
|
+
if (!client) {
|
|
56
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? process.env.SUPABASE_URL
|
|
57
|
+
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? process.env.SUPABASE_ANON_KEY
|
|
58
|
+
if (!url || !key) {
|
|
59
|
+
throw new Error('Missing SUPABASE_URL or SUPABASE_ANON_KEY environment variables')
|
|
60
|
+
}
|
|
61
|
+
client = createClient<Database>(url, key)
|
|
119
62
|
}
|
|
63
|
+
return client
|
|
120
64
|
}
|
|
121
65
|
|
|
122
|
-
|
|
123
|
-
|
|
66
|
+
// Reset for testing
|
|
67
|
+
export function resetClient(): void {
|
|
68
|
+
client = null
|
|
124
69
|
}
|
|
125
70
|
```
|
|
126
71
|
|
|
127
|
-
### Step 3: Health Check
|
|
128
72
|
```typescript
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return { status: 'unhealthy', error: error.message };
|
|
73
|
+
// packages/supabase/src/admin.ts — Server-side only, never bundle in client code
|
|
74
|
+
import { createClient } from '@supabase/supabase-js'
|
|
75
|
+
import type { Database } from './database.types'
|
|
76
|
+
|
|
77
|
+
export function getSupabaseAdmin() {
|
|
78
|
+
const url = process.env.SUPABASE_URL
|
|
79
|
+
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
80
|
+
if (!url || !serviceKey) {
|
|
81
|
+
throw new Error('Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY — server-only')
|
|
140
82
|
}
|
|
83
|
+
return createClient<Database>(url, serviceKey, {
|
|
84
|
+
auth: { autoRefreshToken: false, persistSession: false }
|
|
85
|
+
})
|
|
141
86
|
}
|
|
142
87
|
```
|
|
143
88
|
|
|
144
|
-
|
|
89
|
+
Key detail: The admin client sets `autoRefreshToken: false` and `persistSession: false` because server-side code should never store user sessions.
|
|
90
|
+
|
|
91
|
+
### Step 2: Multi-Tenant RLS via JWT Claims
|
|
92
|
+
|
|
93
|
+
The most scalable Supabase multi-tenant pattern uses a custom JWT claim (`org_id`) combined with RLS policies. Every table includes an `org_id` column, and RLS extracts the tenant from the user's JWT — no application-level filtering needed.
|
|
94
|
+
|
|
95
|
+
```sql
|
|
96
|
+
-- Migration: 20260101000000_create_tenants.sql
|
|
97
|
+
|
|
98
|
+
-- Tenants table
|
|
99
|
+
create table public.tenants (
|
|
100
|
+
id uuid primary key default gen_random_uuid(),
|
|
101
|
+
name text not null,
|
|
102
|
+
slug text unique not null,
|
|
103
|
+
plan text default 'free' check (plan in ('free', 'pro', 'enterprise')),
|
|
104
|
+
created_at timestamptz default now()
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- Tenant membership
|
|
108
|
+
create table public.tenant_members (
|
|
109
|
+
tenant_id uuid references public.tenants(id) on delete cascade,
|
|
110
|
+
user_id uuid references auth.users(id) on delete cascade,
|
|
111
|
+
role text default 'member' check (role in ('owner', 'admin', 'member', 'viewer')),
|
|
112
|
+
primary key (tenant_id, user_id)
|
|
113
|
+
);
|
|
145
114
|
|
|
115
|
+
-- Example tenant-scoped table
|
|
116
|
+
create table public.projects (
|
|
117
|
+
id uuid primary key default gen_random_uuid(),
|
|
118
|
+
org_id uuid not null references public.tenants(id) on delete cascade,
|
|
119
|
+
name text not null,
|
|
120
|
+
created_by uuid references auth.users(id),
|
|
121
|
+
created_at timestamptz default now()
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Enable RLS on all tenant-scoped tables
|
|
125
|
+
alter table public.projects enable row level security;
|
|
126
|
+
|
|
127
|
+
-- RLS policy: users can only see rows belonging to their tenant
|
|
128
|
+
-- The org_id is extracted from the JWT claims set during authentication
|
|
129
|
+
create policy "Tenant isolation" on public.projects
|
|
130
|
+
for all
|
|
131
|
+
using (
|
|
132
|
+
org_id = (auth.jwt() ->> 'org_id')::uuid
|
|
133
|
+
);
|
|
146
134
|
```
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
└─────────────┘
|
|
135
|
+
|
|
136
|
+
The tenant-switching function verifies membership before updating the JWT claim:
|
|
137
|
+
|
|
138
|
+
```sql
|
|
139
|
+
-- Helper function to set org_id in JWT claims after login
|
|
140
|
+
create or replace function public.set_tenant_claim(tenant_id uuid)
|
|
141
|
+
returns void as $$
|
|
142
|
+
begin
|
|
143
|
+
-- Verify user is a member of this tenant
|
|
144
|
+
if not exists (
|
|
145
|
+
select 1 from public.tenant_members
|
|
146
|
+
where tenant_members.tenant_id = set_tenant_claim.tenant_id
|
|
147
|
+
and tenant_members.user_id = auth.uid()
|
|
148
|
+
) then
|
|
149
|
+
raise exception 'Not a member of tenant %', tenant_id;
|
|
150
|
+
end if;
|
|
151
|
+
|
|
152
|
+
-- Set the custom claim
|
|
153
|
+
perform auth.update_user_metadata(
|
|
154
|
+
auth.uid(),
|
|
155
|
+
jsonb_build_object('org_id', tenant_id)
|
|
156
|
+
);
|
|
157
|
+
end;
|
|
158
|
+
$$ language plpgsql security definer;
|
|
172
159
|
```
|
|
173
160
|
|
|
174
|
-
|
|
161
|
+
Key details for multi-tenant RLS:
|
|
162
|
+
|
|
163
|
+
- `auth.jwt() ->> 'org_id'` reads a custom claim from the user's JWT — zero application code needed
|
|
164
|
+
- Every tenant-scoped table must have an `org_id` column and RLS enabled
|
|
165
|
+
- Tenant switching requires updating the JWT claim and re-authenticating
|
|
166
|
+
- For row-level tenant + role permissions, combine `org_id` with a role lookup
|
|
167
|
+
|
|
168
|
+
### Step 3: Framework Integration (Next.js)
|
|
169
|
+
|
|
170
|
+
Server components use the `service_role` key for direct database access. Client components use the `anon` key with RLS protection.
|
|
175
171
|
|
|
176
172
|
```typescript
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
173
|
+
// app/lib/supabase-server.ts — Next.js App Router (server components)
|
|
174
|
+
import { createClient } from '@supabase/supabase-js'
|
|
175
|
+
import { cookies } from 'next/headers'
|
|
176
|
+
import type { Database } from '@my-platform/supabase'
|
|
177
|
+
|
|
178
|
+
export async function getSupabaseServer() {
|
|
179
|
+
const cookieStore = await cookies()
|
|
180
|
+
|
|
181
|
+
return createClient<Database>(
|
|
182
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
183
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
184
|
+
{
|
|
185
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
186
|
+
global: {
|
|
187
|
+
headers: {
|
|
188
|
+
// Forward the user's auth cookie for RLS context
|
|
189
|
+
cookie: cookieStore.toString()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
)
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
// app/projects/page.tsx — Server component with direct DB access
|
|
197
|
+
export default async function ProjectsPage() {
|
|
198
|
+
const supabase = await getSupabaseServer()
|
|
199
|
+
const { data: projects } = await supabase
|
|
200
|
+
.from('projects')
|
|
201
|
+
.select('id, name, created_at')
|
|
202
|
+
.order('created_at', { ascending: false })
|
|
203
|
+
.limit(50)
|
|
204
|
+
|
|
205
|
+
return <ProjectList projects={projects ?? []} />
|
|
192
206
|
}
|
|
193
207
|
```
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
```typescript
|
|
210
|
+
// app/lib/supabase-browser.ts — Client components use the anon key
|
|
211
|
+
'use client'
|
|
212
|
+
import { createClient } from '@supabase/supabase-js'
|
|
213
|
+
import type { Database } from '@my-platform/supabase'
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
Create the singleton client with caching and monitoring.
|
|
215
|
+
let browserClient: ReturnType<typeof createClient<Database>> | null = null
|
|
202
216
|
|
|
203
|
-
|
|
204
|
-
|
|
217
|
+
export function getSupabaseBrowser() {
|
|
218
|
+
if (!browserClient) {
|
|
219
|
+
browserClient = createClient<Database>(
|
|
220
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
221
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
return browserClient
|
|
225
|
+
}
|
|
226
|
+
```
|
|
205
227
|
|
|
206
|
-
|
|
207
|
-
Add health check endpoint for Supabase connectivity.
|
|
228
|
+
For SvelteKit integration and additional framework patterns, see [Examples](references/examples.md).
|
|
208
229
|
|
|
209
230
|
## Output
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
231
|
+
|
|
232
|
+
After applying these patterns you will have:
|
|
233
|
+
|
|
234
|
+
- Monorepo with shared Supabase client, typed database access, and centralized migrations
|
|
235
|
+
- Multi-tenant RLS isolation using `auth.jwt() ->> 'org_id'` — zero application-level filtering
|
|
236
|
+
- Framework-specific integration for Next.js (server/client split) and SvelteKit (hooks)
|
|
237
|
+
- Edge Functions, caching layer, job queue, and audit trail (see [Operational Patterns](references/key-components.md))
|
|
214
238
|
|
|
215
239
|
## Error Handling
|
|
216
|
-
|
|
240
|
+
|
|
241
|
+
| Error | Cause | Solution |
|
|
217
242
|
|-------|-------|----------|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
243
|
+
| `Missing SUPABASE_URL or SUPABASE_ANON_KEY` | Environment variables not set | Check `.env` file and ensure variables are loaded |
|
|
244
|
+
| `new row violates row-level security policy` | RLS blocks the operation | Verify `org_id` JWT claim matches the row's `org_id` |
|
|
245
|
+
| `Not a member of tenant` | User tried switching to unauthorized tenant | Check `tenant_members` table for the user-tenant pair |
|
|
246
|
+
| `TypeError: Cannot read properties of null` | Client singleton not initialized | Ensure env vars are available before first `getSupabaseClient()` call |
|
|
247
|
+
| `cron.schedule: permission denied` | `pg_cron` extension not enabled | Enable via dashboard: Database > Extensions > pg_cron |
|
|
248
|
+
|
|
249
|
+
For the full error reference including RLS debugging and cross-project troubleshooting, see [Error Handling Reference](references/errors.md).
|
|
222
250
|
|
|
223
251
|
## Examples
|
|
224
252
|
|
|
225
|
-
###
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
253
|
+
### Multi-Tenant Query Flow (TypeScript)
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { createClient } from '@supabase/supabase-js'
|
|
257
|
+
import type { Database } from './database.types'
|
|
258
|
+
|
|
259
|
+
const supabase = createClient<Database>(
|
|
260
|
+
process.env.SUPABASE_URL!,
|
|
261
|
+
process.env.SUPABASE_ANON_KEY!
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// 1. Sign in
|
|
265
|
+
const { data: { session } } = await supabase.auth.signInWithPassword({
|
|
266
|
+
email: 'user@example.com',
|
|
267
|
+
password: 'secure-password'
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// 2. Switch tenant context
|
|
271
|
+
const { error: claimError } = await supabase.rpc('set_tenant_claim', {
|
|
272
|
+
tenant_id: 'tenant-uuid-here'
|
|
273
|
+
})
|
|
274
|
+
if (claimError) throw claimError
|
|
275
|
+
|
|
276
|
+
// 3. Refresh session to pick up new JWT claims
|
|
277
|
+
await supabase.auth.refreshSession()
|
|
278
|
+
|
|
279
|
+
// 4. All subsequent queries are automatically scoped to this tenant
|
|
280
|
+
const { data: projects } = await supabase
|
|
281
|
+
.from('projects')
|
|
282
|
+
.select('id, name, created_at')
|
|
283
|
+
.order('created_at', { ascending: false })
|
|
284
|
+
|
|
285
|
+
console.log('Tenant projects:', projects)
|
|
286
|
+
// Only returns projects where org_id matches the JWT claim
|
|
231
287
|
```
|
|
232
288
|
|
|
289
|
+
For the job queue consumer example and SvelteKit integration, see [Examples](references/examples.md).
|
|
290
|
+
|
|
233
291
|
## Resources
|
|
234
|
-
- [Supabase SDK Documentation](https://supabase.com/docs/sdk)
|
|
235
|
-
- [Supabase Best Practices](https://supabase.com/docs/best-practices)
|
|
236
292
|
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
- [Supabase Architecture](https://supabase.com/docs/guides/getting-started/architecture)
|
|
294
|
+
- [Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
|
|
295
|
+
- [Multi-Tenant RLS](https://supabase.com/docs/guides/auth/row-level-security#multi-tenant-applications)
|
|
296
|
+
- [Edge Functions](https://supabase.com/docs/guides/functions)
|
|
297
|
+
- [TypeScript Support](https://supabase.com/docs/reference/javascript/typescript-support)
|
|
298
|
+
- [Generating Types](https://supabase.com/docs/guides/api/rest/generating-types)
|
|
299
|
+
- [pg_cron Extension](https://supabase.com/docs/guides/database/extensions/pg_cron)
|
|
300
|
+
- [Auth JWT Helper](https://supabase.com/docs/guides/auth/jwts)
|
|
301
|
+
- [createClient Reference](https://supabase.com/docs/reference/javascript/initializing)
|
|
302
|
+
|
|
303
|
+
## Next Steps
|
|
304
|
+
|
|
305
|
+
For performance optimization and indexing strategies, see `supabase-performance-tuning`. For deployment pipelines and CI integration, see `supabase-ci-integration`. For security hardening and policy guardrails, see `supabase-security-basics`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Error Handling Reference
|
|
2
|
+
|
|
3
|
+
## Common Errors
|
|
4
|
+
|
|
5
|
+
| Error | Cause | Solution |
|
|
6
|
+
|-------|-------|----------|
|
|
7
|
+
| `Missing SUPABASE_URL or SUPABASE_ANON_KEY` | Environment variables not set | Check `.env` file and ensure variables are loaded |
|
|
8
|
+
| `new row violates row-level security policy` | RLS blocks the operation | Verify `org_id` JWT claim matches the row's `org_id` |
|
|
9
|
+
| `Not a member of tenant` | User tried switching to unauthorized tenant | Check `tenant_members` table for the user-tenant pair |
|
|
10
|
+
| `relation "public.audit_log" does not exist` | Audit migration not applied | Run `supabase db push` or `supabase db reset` |
|
|
11
|
+
| `permission denied for function claim_next_job` | Missing execute grant | Run `grant execute on function claim_next_job to authenticated` |
|
|
12
|
+
| `Cross-project profile lookup failed` | Wrong service_role key for the target project | Verify `MAIN_SUPABASE_SERVICE_ROLE_KEY` matches the main project |
|
|
13
|
+
| `TypeError: Cannot read properties of null` | Client singleton not initialized | Ensure env vars are available before first `getSupabaseClient()` call |
|
|
14
|
+
| `cron.schedule: permission denied` | `pg_cron` extension not enabled | Enable via dashboard: Database > Extensions > pg_cron |
|
|
15
|
+
|
|
16
|
+
## RLS Debugging Checklist
|
|
17
|
+
|
|
18
|
+
1. Verify `alter table ... enable row level security` was run on the table
|
|
19
|
+
2. Check that at least one permissive policy exists for the operation (SELECT, INSERT, UPDATE, DELETE)
|
|
20
|
+
3. Confirm the JWT contains the expected `org_id` claim: `select auth.jwt() ->> 'org_id'`
|
|
21
|
+
4. Test the policy logic directly: `select * from projects where org_id = 'expected-uuid'`
|
|
22
|
+
5. Use `supabase inspect db policies` to list all active policies
|
|
23
|
+
|
|
24
|
+
## Cross-Project Access Troubleshooting
|
|
25
|
+
|
|
26
|
+
- **401 Unauthorized**: The `service_role` key is wrong or belongs to a different project
|
|
27
|
+
- **Network timeout**: Cross-region calls add latency; consider caching or co-locating projects
|
|
28
|
+
- **Rate limited**: Supabase enforces per-project rate limits; distribute load across projects
|
|
29
|
+
- **Type mismatch**: Regenerate types for both projects after schema changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Examples — Multi-Tenant Setup and Job Queue Consumer
|
|
2
|
+
|
|
3
|
+
## Complete Multi-Tenant Setup (TypeScript)
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { createClient } from '@supabase/supabase-js'
|
|
7
|
+
import type { Database } from './database.types'
|
|
8
|
+
|
|
9
|
+
const supabase = createClient<Database>(
|
|
10
|
+
process.env.SUPABASE_URL!,
|
|
11
|
+
process.env.SUPABASE_ANON_KEY!
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// 1. Sign in
|
|
15
|
+
const { data: { session } } = await supabase.auth.signInWithPassword({
|
|
16
|
+
email: 'user@example.com',
|
|
17
|
+
password: 'secure-password'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// 2. Switch tenant context
|
|
21
|
+
const { error: claimError } = await supabase.rpc('set_tenant_claim', {
|
|
22
|
+
tenant_id: 'tenant-uuid-here'
|
|
23
|
+
})
|
|
24
|
+
if (claimError) throw claimError
|
|
25
|
+
|
|
26
|
+
// 3. Refresh session to pick up new JWT claims
|
|
27
|
+
await supabase.auth.refreshSession()
|
|
28
|
+
|
|
29
|
+
// 4. All subsequent queries are automatically scoped to this tenant
|
|
30
|
+
const { data: projects } = await supabase
|
|
31
|
+
.from('projects')
|
|
32
|
+
.select('id, name, created_at')
|
|
33
|
+
.order('created_at', { ascending: false })
|
|
34
|
+
|
|
35
|
+
console.log('Tenant projects:', projects)
|
|
36
|
+
// Only returns projects where org_id matches the JWT claim
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Job Queue Consumer (TypeScript)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { getSupabaseAdmin } from './admin'
|
|
43
|
+
|
|
44
|
+
async function processJobs() {
|
|
45
|
+
const supabase = getSupabaseAdmin()
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
// Atomically claim the next job
|
|
49
|
+
const { data: job, error } = await supabase
|
|
50
|
+
.rpc('claim_next_job', { p_job_type: 'send-email' })
|
|
51
|
+
|
|
52
|
+
if (error || !job) {
|
|
53
|
+
// No jobs available — wait before polling again
|
|
54
|
+
await new Promise(r => setTimeout(r, 5000))
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Process the job
|
|
60
|
+
console.log(`Processing job ${job.id}:`, job.payload)
|
|
61
|
+
await sendEmail(job.payload)
|
|
62
|
+
|
|
63
|
+
// Mark complete
|
|
64
|
+
await supabase
|
|
65
|
+
.from('job_queue')
|
|
66
|
+
.update({ status: 'completed', completed_at: new Date().toISOString() })
|
|
67
|
+
.eq('id', job.id)
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Mark failed — pg_cron will retry if attempts < max_attempts
|
|
70
|
+
await supabase
|
|
71
|
+
.from('job_queue')
|
|
72
|
+
.update({
|
|
73
|
+
status: 'failed',
|
|
74
|
+
error_message: err instanceof Error ? err.message : String(err)
|
|
75
|
+
})
|
|
76
|
+
.eq('id', job.id)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function sendEmail(payload: Record<string, unknown>) {
|
|
82
|
+
console.log('Sending email to:', payload.to)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
processJobs().catch(console.error)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## SvelteKit Integration
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// src/hooks.server.ts — SvelteKit server hooks
|
|
92
|
+
import { createClient } from '@supabase/supabase-js'
|
|
93
|
+
import type { Handle } from '@sveltejs/kit'
|
|
94
|
+
import type { Database } from '@my-platform/supabase'
|
|
95
|
+
|
|
96
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
97
|
+
event.locals.supabase = createClient<Database>(
|
|
98
|
+
import.meta.env.VITE_SUPABASE_URL,
|
|
99
|
+
import.meta.env.VITE_SUPABASE_ANON_KEY,
|
|
100
|
+
{
|
|
101
|
+
global: {
|
|
102
|
+
headers: { cookie: event.request.headers.get('cookie') ?? '' }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Server-side admin client for privileged operations
|
|
108
|
+
event.locals.supabaseAdmin = createClient<Database>(
|
|
109
|
+
import.meta.env.VITE_SUPABASE_URL,
|
|
110
|
+
import.meta.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
111
|
+
{ auth: { autoRefreshToken: false, persistSession: false } }
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return resolve(event)
|
|
115
|
+
}
|
|
116
|
+
```
|