@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,290 +1,285 @@
1
1
  ---
2
2
  name: supabase-reliability-patterns
3
- description: |
4
- Implement Supabase reliability patterns including circuit breakers, idempotency, and graceful degradation.
5
- Use when building fault-tolerant Supabase integrations, implementing retry strategies,
6
- or adding resilience to production Supabase services.
7
- Trigger with phrases like "supabase reliability", "supabase circuit breaker",
8
- "supabase idempotent", "supabase resilience", "supabase fallback", "supabase bulkhead".
9
- allowed-tools: Read, Write, Edit
10
- version: 1.0.0
11
- license: MIT
12
- author: Jeremy Longshore <jeremy@intentsolutions.io>
13
- ---
3
+ description: 'Build resilient Supabase integrations: circuit breakers wrapping createClient
14
4
 
15
- # Supabase Reliability Patterns
5
+ calls, offline queue with IndexedDB, graceful degradation with cached fallbacks,
16
6
 
17
- ## Overview
18
- Production-grade reliability patterns for Supabase integrations.
7
+ health check endpoints, retry with exponential backoff and jitter,
19
8
 
20
- ## Prerequisites
21
- - Understanding of circuit breaker pattern
22
- - opossum or similar library installed
23
- - Queue infrastructure for DLQ
24
- - Caching layer for fallbacks
9
+ and dual-write patterns for critical data.
25
10
 
26
- ## Circuit Breaker
11
+ Use when building fault-tolerant apps, handling Supabase outages gracefully,
27
12
 
28
- ```typescript
29
- import CircuitBreaker from 'opossum';
30
-
31
- const supabaseBreaker = new CircuitBreaker(
32
- async (operation: () => Promise<any>) => operation(),
33
- {
34
- timeout: 30000,
35
- errorThresholdPercentage: 50,
36
- resetTimeout: 30000,
37
- volumeThreshold: 10,
38
- }
39
- );
13
+ implementing offline-first patterns, or adding retry logic to SDK calls.
40
14
 
41
- // Events
42
- supabaseBreaker.on('open', () => {
43
- console.warn('Supabase circuit OPEN - requests failing fast');
44
- alertOps('Supabase circuit breaker opened');
45
- });
15
+ Trigger with phrases like "supabase circuit breaker", "supabase offline",
46
16
 
47
- supabaseBreaker.on('halfOpen', () => {
48
- console.info('Supabase circuit HALF-OPEN - testing recovery');
49
- });
17
+ "supabase retry", "supabase health check", "supabase fallback",
50
18
 
51
- supabaseBreaker.on('close', () => {
52
- console.info('Supabase circuit CLOSED - normal operation');
53
- });
19
+ "supabase resilience", "supabase dual write", "supabase outage".
54
20
 
55
- // Usage
56
- async function safeSupabaseCall<T>(fn: () => Promise<T>): Promise<T> {
57
- return supabaseBreaker.fire(fn);
58
- }
59
- ```
21
+ '
22
+ allowed-tools: Read, Write, Edit, Bash(supabase:*), Bash(curl:*), Grep
23
+ version: 1.0.0
24
+ license: MIT
25
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
26
+ tags:
27
+ - saas
28
+ - supabase
29
+ - reliability
30
+ - resilience
31
+ - circuit-breaker
32
+ - offline
33
+ - retry
34
+ compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
35
+ ---
36
+ # Supabase Reliability Patterns
60
37
 
61
- ## Idempotency Keys
38
+ ## Overview
62
39
 
63
- ```typescript
64
- import { v4 as uuidv4 } from 'uuid';
65
- import crypto from 'crypto';
66
-
67
- // Generate deterministic idempotency key from input
68
- function generateIdempotencyKey(
69
- operation: string,
70
- params: Record<string, any>
71
- ): string {
72
- const data = JSON.stringify({ operation, params });
73
- return crypto.createHash('sha256').update(data).digest('hex');
74
- }
40
+ Production Supabase apps need six reliability layers: **circuit breakers** (stop calling Supabase when it's down to prevent cascading failures), **offline queue** (buffer writes when the network is unavailable and replay when reconnected), **graceful degradation** (serve cached or fallback data during outages), **health checks** (detect Supabase availability before routing traffic), **retry with exponential backoff** (handle transient errors without overwhelming the service), and **dual-write** (write critical data to both Supabase and a backup store). All patterns use real `createClient` from `@supabase/supabase-js`.
75
41
 
76
- // Or use random key with storage
77
- class IdempotencyManager {
78
- private store: Map<string, { key: string; expiresAt: Date }> = new Map();
42
+ ## Prerequisites
79
43
 
80
- getOrCreate(operationId: string): string {
81
- const existing = this.store.get(operationId);
82
- if (existing && existing.expiresAt > new Date()) {
83
- return existing.key;
84
- }
44
+ - `@supabase/supabase-js` v2+ installed
45
+ - TypeScript project with Supabase client configured
46
+ - For offline queue: browser environment with IndexedDB or server with Redis
47
+ - For dual-write: secondary data store (Redis, DynamoDB, or local SQLite)
85
48
 
86
- const key = uuidv4();
87
- this.store.set(operationId, {
88
- key,
89
- expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
90
- });
91
- return key;
92
- }
93
- }
94
- ```
49
+ ## Step 1 — Circuit Breaker and Retry with Exponential Backoff
50
+
51
+ A circuit breaker tracks failures per Supabase service (database, auth, storage) and stops making calls when a threshold is exceeded. Combined with retry logic, it prevents both cascading failures and unnecessary retries during extended outages.
95
52
 
96
- ## Bulkhead Pattern
53
+ ### Circuit Breaker Implementation
97
54
 
98
55
  ```typescript
99
- import PQueue from 'p-queue';
100
-
101
- // Separate queues for different operations
102
- const supabaseQueues = {
103
- critical: new PQueue({ concurrency: 10 }),
104
- normal: new PQueue({ concurrency: 5 }),
105
- bulk: new PQueue({ concurrency: 2 }),
106
- };
107
-
108
- async function prioritizedSupabaseCall<T>(
109
- priority: 'critical' | 'normal' | 'bulk',
110
- fn: () => Promise<T>
111
- ): Promise<T> {
112
- return supabaseQueues[priority].add(fn);
113
- }
56
+ // lib/circuit-breaker.ts
57
+ import { createClient } from '@supabase/supabase-js'
58
+ import type { Database } from './database.types'
114
59
 
115
- // Usage
116
- await prioritizedSupabaseCall('critical', () =>
117
- supabaseClient.processPayment(order)
118
- );
60
+ type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
119
61
 
120
- await prioritizedSupabaseCall('bulk', () =>
121
- supabaseClient.syncCatalog(products)
122
- );
123
- ```
62
+ interface CircuitBreakerOptions {
63
+ failureThreshold: number // failures before opening
64
+ resetTimeoutMs: number // ms before trying again (half-open)
65
+ halfOpenSuccesses: number // successes in half-open to close
66
+ name: string // for logging
67
+ }
124
68
 
125
- ## Timeout Hierarchy
69
+ class CircuitBreaker {
70
+ private state: CircuitState = 'CLOSED'
71
+ private failures = 0
72
+ private lastFailureTime = 0
73
+ private halfOpenSuccesses = 0
74
+
75
+ constructor(private opts: CircuitBreakerOptions) {}
76
+
77
+ async call<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
78
+ // Check if circuit should transition from OPEN to HALF_OPEN
79
+ if (this.state === 'OPEN') {
80
+ if (Date.now() - this.lastFailureTime > this.opts.resetTimeoutMs) {
81
+ this.state = 'HALF_OPEN'
82
+ this.halfOpenSuccesses = 0
83
+ console.log(`[CircuitBreaker:${this.opts.name}] OPEN → HALF_OPEN`)
84
+ } else {
85
+ if (fallback) return fallback()
86
+ throw new Error(`Circuit breaker ${this.opts.name} is OPEN`)
87
+ }
88
+ }
126
89
 
127
- ```typescript
128
- const TIMEOUT_CONFIG = {
129
- connect: 5000, // Initial connection
130
- request: 30000, // Standard requests
131
- upload: 120000, // File uploads
132
- longPoll: 300000, // Webhook long-polling
133
- };
134
-
135
- async function timedoutSupabaseCall<T>(
136
- operation: 'connect' | 'request' | 'upload' | 'longPoll',
137
- fn: () => Promise<T>
138
- ): Promise<T> {
139
- const timeout = TIMEOUT_CONFIG[operation];
140
-
141
- return Promise.race([
142
- fn(),
143
- new Promise<never>((_, reject) =>
144
- setTimeout(() => reject(new Error(`Supabase ${operation} timeout`)), timeout)
145
- ),
146
- ]);
147
- }
148
- ```
90
+ try {
91
+ const result = await fn()
92
+
93
+ // Success in HALF_OPEN: count toward recovery
94
+ if (this.state === 'HALF_OPEN') {
95
+ this.halfOpenSuccesses++
96
+ if (this.halfOpenSuccesses >= this.opts.halfOpenSuccesses) {
97
+ this.state = 'CLOSED'
98
+ this.failures = 0
99
+ console.log(`[CircuitBreaker:${this.opts.name}] HALF_OPEN CLOSED`)
100
+ }
101
+ } else {
102
+ this.failures = 0 // reset on success in CLOSED state
103
+ }
149
104
 
150
- ## Graceful Degradation
105
+ return result
106
+ } catch (error) {
107
+ this.failures++
108
+ this.lastFailureTime = Date.now()
109
+
110
+ if (this.failures >= this.opts.failureThreshold) {
111
+ this.state = 'OPEN'
112
+ console.error(
113
+ `[CircuitBreaker:${this.opts.name}] CLOSED → OPEN after ${this.failures} failures`
114
+ )
115
+ }
151
116
 
152
- ```typescript
153
- interface SupabaseFallback {
154
- enabled: boolean;
155
- data: any;
156
- staleness: 'fresh' | 'stale' | 'very_stale';
157
- }
117
+ if (fallback) return fallback()
118
+ throw error
119
+ }
120
+ }
158
121
 
159
- async function withSupabaseFallback<T>(
160
- fn: () => Promise<T>,
161
- fallbackFn: () => Promise<T>
162
- ): Promise<{ data: T; fallback: boolean }> {
163
- try {
164
- const data = await fn();
165
- // Update cache for future fallback
166
- await updateFallbackCache(data);
167
- return { data, fallback: false };
168
- } catch (error) {
169
- console.warn('Supabase failed, using fallback:', error.message);
170
- const data = await fallbackFn();
171
- return { data, fallback: true };
122
+ get currentState() {
123
+ return { state: this.state, failures: this.failures }
172
124
  }
173
125
  }
126
+
127
+ // One circuit breaker per Supabase service domain
128
+ export const dbCircuit = new CircuitBreaker({
129
+ name: 'database',
130
+ failureThreshold: 5,
131
+ resetTimeoutMs: 30_000,
132
+ halfOpenSuccesses: 3,
133
+ })
134
+
135
+ export const authCircuit = new CircuitBreaker({
136
+ name: 'auth',
137
+ failureThreshold: 3,
138
+ resetTimeoutMs: 15_000,
139
+ halfOpenSuccesses: 2,
140
+ })
141
+
142
+ export const storageCircuit = new CircuitBreaker({
143
+ name: 'storage',
144
+ failureThreshold: 3,
145
+ resetTimeoutMs: 60_000,
146
+ halfOpenSuccesses: 2,
147
+ })
174
148
  ```
175
149
 
176
- ## Dead Letter Queue
150
+ ### Retry with Exponential Backoff and Jitter
177
151
 
178
152
  ```typescript
179
- interface DeadLetterEntry {
180
- id: string;
181
- operation: string;
182
- payload: any;
183
- error: string;
184
- attempts: number;
185
- lastAttempt: Date;
153
+ // lib/retry.ts
154
+ interface RetryOptions {
155
+ maxRetries: number
156
+ baseDelayMs: number
157
+ maxDelayMs: number
158
+ retryableErrors?: string[] // Supabase error codes to retry
186
159
  }
187
160
 
188
- class SupabaseDeadLetterQueue {
189
- private queue: DeadLetterEntry[] = [];
190
-
191
- add(entry: Omit<DeadLetterEntry, 'id' | 'lastAttempt'>): void {
192
- this.queue.push({
193
- ...entry,
194
- id: uuidv4(),
195
- lastAttempt: new Date(),
196
- });
197
- }
198
-
199
- async processOne(): Promise<boolean> {
200
- const entry = this.queue.shift();
201
- if (!entry) return false;
202
-
203
- try {
204
- await supabaseClient[entry.operation](entry.payload);
205
- console.log(`DLQ: Successfully reprocessed ${entry.id}`);
206
- return true;
207
- } catch (error) {
208
- entry.attempts++;
209
- entry.lastAttempt = new Date();
161
+ const DEFAULT_RETRYABLE = [
162
+ 'PGRST301', // connection error
163
+ '08006', // connection failure
164
+ '57014', // query cancelled (timeout)
165
+ '40001', // serialization failure
166
+ '53300', // too many connections
167
+ ]
168
+
169
+ export async function withRetry<T>(
170
+ fn: () => Promise<{ data: T | null; error: any }>,
171
+ opts: RetryOptions = { maxRetries: 3, baseDelayMs: 200, maxDelayMs: 5000 }
172
+ ): Promise<{ data: T | null; error: any }> {
173
+ const retryable = opts.retryableErrors ?? DEFAULT_RETRYABLE
174
+
175
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
176
+ const result = await fn()
177
+
178
+ if (!result.error) return result
179
+
180
+ // Don't retry non-retryable errors (auth, RLS, validation)
181
+ const errorCode = result.error.code ?? ''
182
+ if (!retryable.includes(errorCode) && attempt > 0) {
183
+ return result
184
+ }
210
185
 
211
- if (entry.attempts < 5) {
212
- this.queue.push(entry);
213
- } else {
214
- console.error(`DLQ: Giving up on ${entry.id} after 5 attempts`);
215
- await alertOnPermanentFailure(entry);
216
- }
217
- return false;
186
+ if (attempt < opts.maxRetries) {
187
+ // Exponential backoff with full jitter
188
+ const delay = Math.min(
189
+ opts.baseDelayMs * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5),
190
+ opts.maxDelayMs
191
+ )
192
+ console.warn(
193
+ `[Retry] Attempt ${attempt + 1}/${opts.maxRetries} failed (${errorCode}), ` +
194
+ `retrying in ${Math.round(delay)}ms`
195
+ )
196
+ await new Promise(resolve => setTimeout(resolve, delay))
218
197
  }
219
198
  }
199
+
200
+ // Should not reach here, but return last result
201
+ return fn()
220
202
  }
221
203
  ```
222
204
 
223
- ## Health Check with Degraded State
205
+ ### Combining Circuit Breaker + Retry
224
206
 
225
207
  ```typescript
226
- type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
227
-
228
- async function supabaseHealthCheck(): Promise<{
229
- status: HealthStatus;
230
- details: Record<string, any>;
231
- }> {
232
- const checks = {
233
- api: await checkApiConnectivity(),
234
- circuitBreaker: supabaseBreaker.stats(),
235
- dlqSize: deadLetterQueue.size(),
236
- };
237
-
238
- const status: HealthStatus =
239
- !checks.api.connected ? 'unhealthy' :
240
- checks.circuitBreaker.state === 'open' ? 'degraded' :
241
- checks.dlqSize > 100 ? 'degraded' :
242
- 'healthy';
243
-
244
- return { status, details: checks };
208
+ // services/todo-service.ts
209
+ import { createClient } from '@supabase/supabase-js'
210
+ import type { Database } from '../lib/database.types'
211
+ import { dbCircuit } from '../lib/circuit-breaker'
212
+ import { withRetry } from '../lib/retry'
213
+
214
+ const supabase = createClient<Database>(
215
+ process.env.SUPABASE_URL!,
216
+ process.env.SUPABASE_ANON_KEY!
217
+ )
218
+
219
+ export const TodoService = {
220
+ async list(userId: string) {
221
+ return dbCircuit.call(
222
+ // Retry transient errors inside the circuit breaker
223
+ () => withRetry(() =>
224
+ supabase
225
+ .from('todos')
226
+ .select('id, title, is_complete, created_at')
227
+ .eq('user_id', userId)
228
+ .order('created_at', { ascending: false })
229
+ ),
230
+ // Fallback when circuit is OPEN: return empty list
231
+ () => ({ data: [], error: null })
232
+ )
233
+ },
234
+
235
+ async create(todo: { title: string; user_id: string }) {
236
+ return dbCircuit.call(
237
+ () => withRetry(() =>
238
+ supabase
239
+ .from('todos')
240
+ .insert(todo)
241
+ .select('id, title, is_complete, created_at')
242
+ .single()
243
+ )
244
+ // No fallback for writes — let the error propagate to the offline queue
245
+ )
246
+ },
245
247
  }
246
248
  ```
247
249
 
248
- ## Instructions
249
-
250
- ### Step 1: Implement Circuit Breaker
251
- Wrap Supabase calls with circuit breaker.
252
-
253
- ### Step 2: Add Idempotency Keys
254
- Generate deterministic keys for operations.
255
-
256
- ### Step 3: Configure Bulkheads
257
- Separate queues for different priorities.
250
+ ## Step 2 — Offline Queue and Graceful Degradation
258
251
 
259
- ### Step 4: Set Up Dead Letter Queue
260
- Handle permanent failures gracefully.
252
+ See [offline queue, graceful degradation, health checks, and dual-write](references/offline-degradation-health-dualwrite.md) for IndexedDB offline queue with auto-flush, cached fallback patterns, health check endpoints monitoring database/auth/storage, dual-write for critical data with Redis backup, and reconciliation jobs.
261
253
 
262
254
  ## Output
263
- - Circuit breaker protecting Supabase calls
264
- - Idempotency preventing duplicates
265
- - Bulkhead isolation implemented
266
- - DLQ for failed operations
255
+
256
+ - Circuit breaker protecting database, auth, and storage calls independently
257
+ - Retry with exponential backoff and jitter for transient Supabase errors
258
+ - Offline queue buffering writes during network outages with auto-flush on reconnect
259
+ - Graceful degradation serving cached or fallback data when Supabase is unavailable
260
+ - Health check endpoint monitoring all three Supabase services
261
+ - Dual-write pattern ensuring critical data survives Supabase outages
262
+ - Reconciliation job detecting and replaying missing records
267
263
 
268
264
  ## Error Handling
265
+
269
266
  | Issue | Cause | Solution |
270
267
  |-------|-------|----------|
271
- | Circuit stays open | Threshold too low | Adjust error percentage |
272
- | Duplicate operations | Missing idempotency | Add idempotency key |
273
- | Queue full | Rate too high | Increase concurrency |
274
- | DLQ growing | Persistent failures | Investigate root cause |
275
-
276
- ## Examples
277
-
278
- ### Quick Circuit Check
279
- ```typescript
280
- const state = supabaseBreaker.stats().state;
281
- console.log('Supabase circuit:', state);
282
- ```
268
+ | Circuit stays OPEN indefinitely | Supabase extended outage | Monitor `status.supabase.com`; circuit auto-recovers via HALF_OPEN |
269
+ | Offline queue grows unbounded | User offline for hours | Cap queue at 1000 items; show UI warning at 100 |
270
+ | Stale cache served too long | Cache TTL too generous | Reduce `cacheTtlMs`; show "last updated" timestamp |
271
+ | Dual-write divergence | Network partition | Run reconciliation job every 5 min; alert on divergence count |
272
+ | Health check false positive | Transient network blip | Require 3 consecutive failures before marking unhealthy |
273
+ | Retry storm | All clients retry simultaneously | Jitter prevents thundering herd; circuit breaker stops retries when open |
283
274
 
284
275
  ## Resources
285
- - [Circuit Breaker Pattern](https://martinfowler.com/bliki/CircuitBreaker.html)
286
- - [Opossum Documentation](https://nodeshift.dev/opossum/)
287
- - [Supabase Reliability Guide](https://supabase.com/docs/reliability)
276
+
277
+ - [Circuit Breaker Pattern (Martin Fowler)](https://martinfowler.com/bliki/CircuitBreaker.html)
278
+ - [Exponential Backoff (AWS)](https://docs.aws.amazon.com/general/latest/gr/api-retries.html)
279
+ - [Supabase Status Page](https://status.supabase.com)
280
+ - [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
281
+ - [Supabase Realtime (connection recovery)](https://supabase.com/docs/guides/realtime)
288
282
 
289
283
  ## Next Steps
290
- For policy enforcement, see `supabase-policy-guardrails`.
284
+
285
+ For organizational governance and policy guardrails, see `supabase-policy-guardrails`.
@@ -0,0 +1,36 @@
1
+ # Circuit Breaker
2
+
3
+ ## Circuit Breaker
4
+
5
+ ```typescript
6
+ import CircuitBreaker from 'opossum';
7
+
8
+ const supabaseBreaker = new CircuitBreaker(
9
+ async (operation: () => Promise<any>) => operation(),
10
+ {
11
+ timeout: 30000,
12
+ errorThresholdPercentage: 50,
13
+ resetTimeout: 30000,
14
+ volumeThreshold: 10,
15
+ }
16
+ );
17
+
18
+ // Events
19
+ supabaseBreaker.on('open', () => {
20
+ console.warn('Supabase circuit OPEN - requests failing fast');
21
+ alertOps('Supabase circuit breaker opened');
22
+ });
23
+
24
+ supabaseBreaker.on('halfOpen', () => {
25
+ console.info('Supabase circuit HALF-OPEN - testing recovery');
26
+ });
27
+
28
+ supabaseBreaker.on('close', () => {
29
+ console.info('Supabase circuit CLOSED - normal operation');
30
+ });
31
+
32
+ // Usage
33
+ async function safeSupabaseCall<T>(fn: () => Promise<T>): Promise<T> {
34
+ return supabaseBreaker.fire(fn);
35
+ }
36
+ ```
@@ -0,0 +1,48 @@
1
+ # Dead Letter Queue
2
+
3
+ ## Dead Letter Queue
4
+
5
+ ```typescript
6
+ interface DeadLetterEntry {
7
+ id: string;
8
+ operation: string;
9
+ payload: any;
10
+ error: string;
11
+ attempts: number;
12
+ lastAttempt: Date;
13
+ }
14
+
15
+ class SupabaseDeadLetterQueue {
16
+ private queue: DeadLetterEntry[] = [];
17
+
18
+ add(entry: Omit<DeadLetterEntry, 'id' | 'lastAttempt'>): void {
19
+ this.queue.push({
20
+ ...entry,
21
+ id: uuidv4(),
22
+ lastAttempt: new Date(),
23
+ });
24
+ }
25
+
26
+ async processOne(): Promise<boolean> {
27
+ const entry = this.queue.shift();
28
+ if (!entry) return false;
29
+
30
+ try {
31
+ await supabaseCliententry.operation;
32
+ console.log(`DLQ: Successfully reprocessed ${entry.id}`);
33
+ return true;
34
+ } catch (error) {
35
+ entry.attempts++;
36
+ entry.lastAttempt = new Date();
37
+
38
+ if (entry.attempts < 5) {
39
+ this.queue.push(entry);
40
+ } else {
41
+ console.error(`DLQ: Giving up on ${entry.id} after 5 attempts`);
42
+ await alertOnPermanentFailure(entry);
43
+ }
44
+ return false;
45
+ }
46
+ }
47
+ }
48
+ ```
@@ -0,0 +1,11 @@
1
+ # Error Handling Reference
2
+
3
+ | Issue | Cause | Solution |
4
+ |-------|-------|----------|
5
+ | Circuit stays open | Threshold too low | Adjust error percentage |
6
+ | Duplicate operations | Missing idempotency | Add idempotency key |
7
+ | Queue full | Rate too high | Increase concurrency |
8
+ | DLQ growing | Persistent failures | Investigate root cause |
9
+
10
+ ---
11
+ *[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
@@ -0,0 +1,11 @@
1
+ ## Examples
2
+
3
+ ### Quick Circuit Check
4
+
5
+ ```typescript
6
+ const state = supabaseBreaker.stats().state;
7
+ console.log('Supabase circuit:', state);
8
+ ```
9
+
10
+ ---
11
+ *[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*
@@ -0,0 +1,36 @@
1
+ # Idempotency Keys
2
+
3
+ ## Idempotency Keys
4
+
5
+ ```typescript
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import crypto from 'crypto';
8
+
9
+ // Generate deterministic idempotency key from input
10
+ function generateIdempotencyKey(
11
+ operation: string,
12
+ params: Record<string, any>
13
+ ): string {
14
+ const data = JSON.stringify({ operation, params });
15
+ return crypto.createHash('sha256').update(data).digest('hex');
16
+ }
17
+
18
+ // Or use random key with storage
19
+ class IdempotencyManager {
20
+ private store: Map<string, { key: string; expiresAt: Date }> = new Map();
21
+
22
+ getOrCreate(operationId: string): string {
23
+ const existing = this.store.get(operationId);
24
+ if (existing && existing.expiresAt > new Date()) {
25
+ return existing.key;
26
+ }
27
+
28
+ const key = uuidv4();
29
+ this.store.set(operationId, {
30
+ key,
31
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
32
+ });
33
+ return key;
34
+ }
35
+ }
36
+ ```