@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
@@ -0,0 +1,489 @@
1
+ ## Offline Queue, Graceful Degradation, Health Checks, and Dual-Write
2
+
3
+ ### Offline Queue with IndexedDB (Browser)
4
+
5
+ ```typescript
6
+ // lib/offline-queue.ts
7
+ import { createClient } from '@supabase/supabase-js'
8
+ import type { Database } from './database.types'
9
+
10
+ interface QueuedOperation {
11
+ id: string
12
+ table: string
13
+ method: 'insert' | 'update' | 'delete'
14
+ payload: any
15
+ createdAt: number
16
+ retries: number
17
+ }
18
+
19
+ class OfflineQueue {
20
+ private db: IDBDatabase | null = null
21
+ private readonly STORE = 'pending_operations'
22
+ private processing = false
23
+
24
+ async init() {
25
+ return new Promise<void>((resolve, reject) => {
26
+ const request = indexedDB.open('supabase_offline_queue', 1)
27
+ request.onupgradeneeded = () => {
28
+ const db = request.result
29
+ if (!db.objectStoreNames.contains(this.STORE)) {
30
+ db.createObjectStore(this.STORE, { keyPath: 'id' })
31
+ }
32
+ }
33
+ request.onsuccess = () => { this.db = request.result; resolve() }
34
+ request.onerror = () => reject(request.error)
35
+ })
36
+ }
37
+
38
+ async enqueue(op: Omit<QueuedOperation, 'id' | 'createdAt' | 'retries'>) {
39
+ if (!this.db) await this.init()
40
+
41
+ const entry: QueuedOperation = {
42
+ ...op,
43
+ id: crypto.randomUUID(),
44
+ createdAt: Date.now(),
45
+ retries: 0,
46
+ }
47
+
48
+ const tx = this.db!.transaction(this.STORE, 'readwrite')
49
+ tx.objectStore(this.STORE).add(entry)
50
+ await new Promise(resolve => { tx.oncomplete = resolve })
51
+
52
+ console.log(`[OfflineQueue] Enqueued ${op.method} on ${op.table}`)
53
+ return entry.id
54
+ }
55
+
56
+ async flush(supabase: ReturnType<typeof createClient<Database>>) {
57
+ if (this.processing || !this.db) return
58
+ this.processing = true
59
+
60
+ try {
61
+ const tx = this.db.transaction(this.STORE, 'readonly')
62
+ const store = tx.objectStore(this.STORE)
63
+ const request = store.getAll()
64
+ const items: QueuedOperation[] = await new Promise(resolve => {
65
+ request.onsuccess = () => resolve(request.result)
66
+ })
67
+
68
+ for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
69
+ try {
70
+ let result: any
71
+
72
+ if (item.method === 'insert') {
73
+ result = await supabase.from(item.table).insert(item.payload).select()
74
+ } else if (item.method === 'update') {
75
+ const { id, ...updates } = item.payload
76
+ result = await supabase.from(item.table).update(updates).eq('id', id)
77
+ } else if (item.method === 'delete') {
78
+ result = await supabase.from(item.table).delete().eq('id', item.payload.id)
79
+ }
80
+
81
+ if (result?.error) throw result.error
82
+
83
+ // Remove from queue on success
84
+ const deleteTx = this.db!.transaction(this.STORE, 'readwrite')
85
+ deleteTx.objectStore(this.STORE).delete(item.id)
86
+ console.log(`[OfflineQueue] Flushed ${item.method} on ${item.table}`)
87
+ } catch (error) {
88
+ console.warn(`[OfflineQueue] Failed to flush ${item.id}:`, error)
89
+ // Increment retry count; give up after 10 attempts
90
+ if (item.retries >= 10) {
91
+ const deleteTx = this.db!.transaction(this.STORE, 'readwrite')
92
+ deleteTx.objectStore(this.STORE).delete(item.id)
93
+ console.error(`[OfflineQueue] Gave up on ${item.id} after 10 retries`)
94
+ } else {
95
+ const updateTx = this.db!.transaction(this.STORE, 'readwrite')
96
+ updateTx.objectStore(this.STORE).put({ ...item, retries: item.retries + 1 })
97
+ }
98
+ }
99
+ }
100
+ } finally {
101
+ this.processing = false
102
+ }
103
+ }
104
+
105
+ get pendingCount(): Promise<number> {
106
+ if (!this.db) return Promise.resolve(0)
107
+ const tx = this.db.transaction(this.STORE, 'readonly')
108
+ const request = tx.objectStore(this.STORE).count()
109
+ return new Promise(resolve => { request.onsuccess = () => resolve(request.result) })
110
+ }
111
+ }
112
+
113
+ export const offlineQueue = new OfflineQueue()
114
+
115
+ // Auto-flush when back online
116
+ if (typeof window !== 'undefined') {
117
+ window.addEventListener('online', async () => {
118
+ const supabase = createClient<Database>(
119
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
120
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
121
+ )
122
+ await offlineQueue.flush(supabase)
123
+ })
124
+ }
125
+ ```
126
+
127
+ ### Graceful Degradation with Cached Fallbacks
128
+
129
+ ```typescript
130
+ // lib/degradation.ts
131
+ import { createClient } from '@supabase/supabase-js'
132
+ import type { Database } from './database.types'
133
+
134
+ interface DegradedResponse<T> {
135
+ data: T
136
+ degraded: boolean
137
+ source: 'live' | 'cache' | 'fallback'
138
+ cachedAt?: number
139
+ }
140
+
141
+ const cache = new Map<string, { data: any; timestamp: number }>()
142
+
143
+ export async function withDegradation<T>(
144
+ cacheKey: string,
145
+ liveFn: () => Promise<{ data: T | null; error: any }>,
146
+ fallbackValue: T,
147
+ cacheTtlMs = 5 * 60 * 1000 // 5 min default
148
+ ): Promise<DegradedResponse<T>> {
149
+ // Try live data first
150
+ try {
151
+ const { data, error } = await liveFn()
152
+ if (!error && data !== null) {
153
+ // Update cache on success
154
+ cache.set(cacheKey, { data, timestamp: Date.now() })
155
+ return { data, degraded: false, source: 'live' }
156
+ }
157
+ // Fall through to cache on error
158
+ if (error) console.warn(`[Degradation] Live fetch failed: ${error.message}`)
159
+ } catch (err) {
160
+ console.warn('[Degradation] Live fetch threw:', err)
161
+ }
162
+
163
+ // Try cached data
164
+ const cached = cache.get(cacheKey)
165
+ if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
166
+ return {
167
+ data: cached.data as T,
168
+ degraded: true,
169
+ source: 'cache',
170
+ cachedAt: cached.timestamp,
171
+ }
172
+ }
173
+
174
+ // Last resort: static fallback
175
+ return { data: fallbackValue, degraded: true, source: 'fallback' }
176
+ }
177
+
178
+ // Usage
179
+ const supabase = createClient<Database>(
180
+ process.env.SUPABASE_URL!,
181
+ process.env.SUPABASE_ANON_KEY!
182
+ )
183
+
184
+ async function getProducts(categoryId: string) {
185
+ const result = await withDegradation(
186
+ `products:${categoryId}`,
187
+ () => supabase
188
+ .from('products')
189
+ .select('id, name, price, image_url')
190
+ .eq('category_id', categoryId)
191
+ .order('name'),
192
+ [] // fallback: empty product list
193
+ )
194
+
195
+ if (result.degraded) {
196
+ console.log(`Serving ${result.source} data for products`)
197
+ // Show banner: "Some data may be stale"
198
+ }
199
+
200
+ return result
201
+ }
202
+ ```
203
+
204
+ ## Step 3 — Health Checks and Dual-Write for Critical Data
205
+
206
+ ### Health Check Endpoint
207
+
208
+ ```typescript
209
+ // api/health.ts (Next.js API route or Express handler)
210
+ import { createClient } from '@supabase/supabase-js'
211
+
212
+ interface HealthStatus {
213
+ status: 'healthy' | 'degraded' | 'unhealthy'
214
+ services: {
215
+ database: { ok: boolean; latencyMs: number }
216
+ auth: { ok: boolean; latencyMs: number }
217
+ storage: { ok: boolean; latencyMs: number }
218
+ }
219
+ timestamp: string
220
+ }
221
+
222
+ export async function checkHealth(): Promise<HealthStatus> {
223
+ const supabase = createClient(
224
+ process.env.SUPABASE_URL!,
225
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
226
+ { auth: { autoRefreshToken: false, persistSession: false } }
227
+ )
228
+
229
+ const checks = await Promise.allSettled([
230
+ // Database health: simple query
231
+ (async () => {
232
+ const start = Date.now()
233
+ const { error } = await supabase.from('health_checks').select('id').limit(1)
234
+ return { ok: !error, latencyMs: Date.now() - start }
235
+ })(),
236
+
237
+ // Auth health: verify service key works
238
+ (async () => {
239
+ const start = Date.now()
240
+ const { error } = await supabase.auth.admin.listUsers({ perPage: 1 })
241
+ return { ok: !error, latencyMs: Date.now() - start }
242
+ })(),
243
+
244
+ // Storage health: list buckets
245
+ (async () => {
246
+ const start = Date.now()
247
+ const { error } = await supabase.storage.listBuckets()
248
+ return { ok: !error, latencyMs: Date.now() - start }
249
+ })(),
250
+ ])
251
+
252
+ const [db, auth, storage] = checks.map(r =>
253
+ r.status === 'fulfilled' ? r.value : { ok: false, latencyMs: -1 }
254
+ )
255
+
256
+ const allOk = db.ok && auth.ok && storage.ok
257
+ const anyOk = db.ok || auth.ok || storage.ok
258
+
259
+ return {
260
+ status: allOk ? 'healthy' : anyOk ? 'degraded' : 'unhealthy',
261
+ services: { database: db, auth, storage },
262
+ timestamp: new Date().toISOString(),
263
+ }
264
+ }
265
+
266
+ // Periodic health check (call every 30s)
267
+ let lastHealth: HealthStatus | null = null
268
+
269
+ setInterval(async () => {
270
+ lastHealth = await checkHealth()
271
+ if (lastHealth.status !== 'healthy') {
272
+ console.warn('[HealthCheck]', JSON.stringify(lastHealth))
273
+ // Alert via webhook, PagerDuty, etc.
274
+ }
275
+ }, 30_000)
276
+
277
+ export function getLastHealth() { return lastHealth }
278
+ ```
279
+
280
+ ### Dual-Write for Critical Data
281
+
282
+ For operations where data loss is unacceptable (payments, audit logs), write to both Supabase and a backup store. Reconcile later if they diverge.
283
+
284
+ ```typescript
285
+ // lib/dual-write.ts
286
+ import { createClient } from '@supabase/supabase-js'
287
+ import type { Database } from './database.types'
288
+ import Redis from 'ioredis'
289
+
290
+ const supabase = createClient<Database>(
291
+ process.env.SUPABASE_URL!,
292
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
293
+ { auth: { autoRefreshToken: false, persistSession: false } }
294
+ )
295
+
296
+ const redis = new Redis(process.env.REDIS_URL!)
297
+
298
+ interface DualWriteResult<T> {
299
+ primary: { data: T | null; error: any }
300
+ backup: { ok: boolean; error?: string }
301
+ }
302
+
303
+ export async function dualWrite<T>(
304
+ table: string,
305
+ record: Record<string, any>,
306
+ selectColumns: string
307
+ ): Promise<DualWriteResult<T>> {
308
+ // Write to both stores concurrently
309
+ const [primary, backup] = await Promise.allSettled([
310
+ // Primary: Supabase
311
+ supabase
312
+ .from(table)
313
+ .insert(record)
314
+ .select(selectColumns)
315
+ .single(),
316
+
317
+ // Backup: Redis with 30-day TTL
318
+ redis.set(
319
+ `backup:${table}:${record.id ?? crypto.randomUUID()}`,
320
+ JSON.stringify({ ...record, _written_at: new Date().toISOString() }),
321
+ 'EX', 30 * 24 * 60 * 60
322
+ ),
323
+ ])
324
+
325
+ const primaryResult = primary.status === 'fulfilled'
326
+ ? primary.value
327
+ : { data: null, error: primary.reason }
328
+
329
+ const backupResult = backup.status === 'fulfilled'
330
+ ? { ok: true }
331
+ : { ok: false, error: String(backup.reason) }
332
+
333
+ // Log if writes diverge
334
+ if (primaryResult.error && backupResult.ok) {
335
+ console.error(`[DualWrite] Primary failed but backup succeeded for ${table}`)
336
+ } else if (!primaryResult.error && !backupResult.ok) {
337
+ console.warn(`[DualWrite] Primary succeeded but backup failed for ${table}`)
338
+ }
339
+
340
+ return { primary: primaryResult, backup: backupResult }
341
+ }
342
+
343
+ // Usage: payment processing
344
+ async function recordPayment(payment: {
345
+ order_id: string
346
+ amount: number
347
+ currency: string
348
+ stripe_payment_id: string
349
+ }) {
350
+ const result = await dualWrite<Database['public']['Tables']['payments']['Row']>(
351
+ 'payments',
352
+ {
353
+ ...payment,
354
+ id: crypto.randomUUID(),
355
+ status: 'completed',
356
+ created_at: new Date().toISOString(),
357
+ },
358
+ 'id, order_id, amount, status, created_at'
359
+ )
360
+
361
+ if (result.primary.error) {
362
+ // Primary write failed — payment is in Redis backup
363
+ // Trigger reconciliation job
364
+ console.error('Payment write failed, queued for reconciliation:', result.primary.error)
365
+ throw new Error(`Payment recording failed: ${result.primary.error.message}`)
366
+ }
367
+
368
+ return result.primary.data
369
+ }
370
+ ```
371
+
372
+ ### Reconciliation Job for Dual-Write
373
+
374
+ ```typescript
375
+ // jobs/reconcile-payments.ts
376
+ import { createClient } from '@supabase/supabase-js'
377
+ import Redis from 'ioredis'
378
+
379
+ const supabase = createClient(
380
+ process.env.SUPABASE_URL!,
381
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
382
+ { auth: { autoRefreshToken: false, persistSession: false } }
383
+ )
384
+
385
+ const redis = new Redis(process.env.REDIS_URL!)
386
+
387
+ export async function reconcilePayments() {
388
+ // Find all backup records that might not be in Supabase
389
+ const keys = await redis.keys('backup:payments:*')
390
+
391
+ for (const key of keys) {
392
+ const raw = await redis.get(key)
393
+ if (!raw) continue
394
+
395
+ const record = JSON.parse(raw)
396
+
397
+ // Check if it exists in Supabase
398
+ const { data } = await supabase
399
+ .from('payments')
400
+ .select('id')
401
+ .eq('id', record.id)
402
+ .maybeSingle()
403
+
404
+ if (!data) {
405
+ // Missing from Supabase — replay the write
406
+ const { error } = await supabase
407
+ .from('payments')
408
+ .insert(record)
409
+ .select()
410
+
411
+ if (!error) {
412
+ await redis.del(key)
413
+ console.log(`[Reconcile] Replayed payment ${record.id}`)
414
+ } else {
415
+ console.error(`[Reconcile] Failed to replay ${record.id}:`, error.message)
416
+ }
417
+ } else {
418
+ // Already in Supabase — clean up backup
419
+ await redis.del(key)
420
+ }
421
+ }
422
+ }
423
+ ```
424
+
425
+ ## Output
426
+
427
+ - Circuit breaker protecting database, auth, and storage calls independently
428
+ - Retry with exponential backoff and jitter for transient Supabase errors
429
+ - Offline queue buffering writes during network outages with auto-flush on reconnect
430
+ - Graceful degradation serving cached or fallback data when Supabase is unavailable
431
+ - Health check endpoint monitoring all three Supabase services
432
+ - Dual-write pattern ensuring critical data survives Supabase outages
433
+ - Reconciliation job detecting and replaying missing records
434
+
435
+ ## Error Handling
436
+
437
+ | Issue | Cause | Solution |
438
+ |-------|-------|----------|
439
+ | Circuit stays OPEN indefinitely | Supabase extended outage | Monitor `status.supabase.com`; circuit auto-recovers via HALF_OPEN |
440
+ | Offline queue grows unbounded | User offline for hours | Cap queue at 1000 items; show UI warning at 100 |
441
+ | Stale cache served too long | Cache TTL too generous | Reduce `cacheTtlMs`; show "last updated" timestamp |
442
+ | Dual-write divergence | Network partition | Run reconciliation job every 5 min; alert on divergence count |
443
+ | Health check false positive | Transient network blip | Require 3 consecutive failures before marking unhealthy |
444
+ | Retry storm | All clients retry simultaneously | Jitter prevents thundering herd; circuit breaker stops retries when open |
445
+
446
+ ## Examples
447
+
448
+ ### Quick Health Check from CLI
449
+
450
+ ```bash
451
+ curl -s https://your-app.com/api/health | jq '.services'
452
+ ```
453
+
454
+ ### Check Circuit Breaker Status
455
+
456
+ ```typescript
457
+ import { dbCircuit, authCircuit, storageCircuit } from '../lib/circuit-breaker'
458
+
459
+ function getCircuitStatus() {
460
+ return {
461
+ database: dbCircuit.currentState,
462
+ auth: authCircuit.currentState,
463
+ storage: storageCircuit.currentState,
464
+ }
465
+ }
466
+ ```
467
+
468
+ ### Test Offline Queue Pending Count
469
+
470
+ ```typescript
471
+ import { offlineQueue } from '../lib/offline-queue'
472
+
473
+ const pending = await offlineQueue.pendingCount
474
+ if (pending > 0) {
475
+ showBanner(`${pending} changes waiting to sync`)
476
+ }
477
+ ```
478
+
479
+ ## Resources
480
+
481
+ - [Circuit Breaker Pattern (Martin Fowler)](https://martinfowler.com/bliki/CircuitBreaker.html)
482
+ - [Exponential Backoff (AWS)](https://docs.aws.amazon.com/general/latest/gr/api-retries.html)
483
+ - [Supabase Status Page](https://status.supabase.com)
484
+ - [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
485
+ - [Supabase Realtime (connection recovery)](https://supabase.com/docs/guides/realtime)
486
+
487
+ ## Next Steps
488
+
489
+ For organizational governance and policy guardrails, see `supabase-policy-guardrails`.