@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,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
|
-
|
|
5
|
+
calls, offline queue with IndexedDB, graceful degradation with cached fallbacks,
|
|
16
6
|
|
|
17
|
-
|
|
18
|
-
Production-grade reliability patterns for Supabase integrations.
|
|
7
|
+
health check endpoints, retry with exponential backoff and jitter,
|
|
19
8
|
|
|
20
|
-
|
|
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
|
-
|
|
11
|
+
Use when building fault-tolerant apps, handling Supabase outages gracefully,
|
|
27
12
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
console.info('Supabase circuit HALF-OPEN - testing recovery');
|
|
49
|
-
});
|
|
17
|
+
"supabase retry", "supabase health check", "supabase fallback",
|
|
50
18
|
|
|
51
|
-
|
|
52
|
-
console.info('Supabase circuit CLOSED - normal operation');
|
|
53
|
-
});
|
|
19
|
+
"supabase resilience", "supabase dual write", "supabase outage".
|
|
54
20
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
##
|
|
38
|
+
## Overview
|
|
62
39
|
|
|
63
|
-
|
|
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
|
-
|
|
77
|
-
class IdempotencyManager {
|
|
78
|
-
private store: Map<string, { key: string; expiresAt: Date }> = new Map();
|
|
42
|
+
## Prerequisites
|
|
79
43
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
53
|
+
### Circuit Breaker Implementation
|
|
97
54
|
|
|
98
55
|
```typescript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
116
|
-
await prioritizedSupabaseCall('critical', () =>
|
|
117
|
-
supabaseClient.processPayment(order)
|
|
118
|
-
);
|
|
60
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
|
|
119
61
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
staleness: 'fresh' | 'stale' | 'very_stale';
|
|
157
|
-
}
|
|
117
|
+
if (fallback) return fallback()
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
158
121
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
150
|
+
### Retry with Exponential Backoff and Jitter
|
|
177
151
|
|
|
178
152
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
205
|
+
### Combining Circuit Breaker + Retry
|
|
224
206
|
|
|
225
207
|
```typescript
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
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
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
286
|
-
- [
|
|
287
|
-
- [
|
|
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
|
-
|
|
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
|
+
```
|