better-supabase 0.4.0 → 0.5.0

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 (179) hide show
  1. package/README.md +2 -0
  2. package/dist/channel-FQW-CnGR.d.ts +70 -0
  3. package/dist/cli/bin.js +1 -1
  4. package/dist/cli/codemod-DqV7612m.js +388 -0
  5. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  6. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  7. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  8. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  9. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  10. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  11. package/dist/cli/{doctor-BOym3mbL.js → doctor-L7Z8mYTx.js} +531 -50
  12. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  13. package/dist/cli/gen-BJc9vdyu.js +2 -0
  14. package/dist/cli/{gen-Ccn_p90L.js → gen-C2osRcr5.js} +283 -87
  15. package/dist/cli/index.d.ts +1 -1
  16. package/dist/cli/index.js +4 -4
  17. package/dist/cli/{init-ClnxRYBF.js → init-Saf5-R9T.js} +2 -2
  18. package/dist/cli/{introspect-DxqfkVIv.js → introspect--H7oGmcd.js} +2 -2
  19. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  20. package/dist/cli/read-sets-BfMQcKy8.js +289 -0
  21. package/dist/cli/{run-CP20LKk_.js → run-BKR2W_79.js} +16 -12
  22. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  23. package/dist/cli/sql-DzPbfngx.js +348 -0
  24. package/dist/cli/{version-5mW_8sY1.js → version-BDJuO0UR.js} +1 -1
  25. package/dist/client/index.d.ts +1 -1
  26. package/dist/client/index.js +3 -10
  27. package/dist/clone-BfgmLU90.js +21 -0
  28. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  29. package/dist/config/index.d.ts +4 -4
  30. package/dist/config/index.js +4 -4
  31. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  32. package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
  33. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  34. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  35. package/dist/edge/index.d.ts +16 -5
  36. package/dist/edge/index.js +10 -7
  37. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-DUxU4oHe.d.ts} +1 -1
  38. package/dist/env/index.d.ts +2 -2
  39. package/dist/env/index.js +34 -1
  40. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  41. package/dist/events/index.d.ts +3 -2
  42. package/dist/events/index.js +65 -12
  43. package/dist/events-aIO7vsIk.d.ts +230 -0
  44. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  45. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  46. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  47. package/dist/hono/index.d.ts +17 -6
  48. package/dist/hono/index.js +40 -5
  49. package/dist/impersonation-VfaGc4Q_.js +41 -0
  50. package/dist/{index-BBdvo-5d.d.ts → index-BGdgBPdf.d.ts} +49 -6
  51. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  52. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  53. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  54. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  55. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  56. package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
  57. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  58. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  59. package/dist/index.d.ts +30 -13
  60. package/dist/index.js +10 -9
  61. package/dist/jobs/index.d.ts +147 -101
  62. package/dist/jobs/index.js +744 -237
  63. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  64. package/dist/kit-events-CTFDfSwb.js +60 -0
  65. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  66. package/dist/kit-transport-DJlHQUBU.js +55 -0
  67. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  68. package/dist/list/index.d.ts +1 -1
  69. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  70. package/dist/mcp/index.d.ts +25 -8
  71. package/dist/mcp/index.js +23 -14
  72. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-CusDgPaD.d.ts} +5 -1
  73. package/dist/next/index.d.ts +31 -10
  74. package/dist/next/index.js +136 -27
  75. package/dist/notifications/index.d.ts +162 -0
  76. package/dist/notifications/index.js +264 -0
  77. package/dist/openapi/index.d.ts +1 -1
  78. package/dist/openapi/index.js +2 -2
  79. package/dist/orgs/index.d.ts +132 -0
  80. package/dist/orgs/index.js +245 -0
  81. package/dist/orpc/index.d.ts +28 -8
  82. package/dist/orpc/index.js +16 -3
  83. package/dist/otel/index.d.ts +21 -2
  84. package/dist/otel/index.js +50 -9
  85. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  86. package/dist/permdock-sql-DgOfeixi.js +51 -0
  87. package/dist/plugin-DwARMuQp.js +59 -0
  88. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  89. package/dist/plugins/actor/index.d.ts +5 -2
  90. package/dist/plugins/actor/index.js +22 -8
  91. package/dist/plugins/rules/index.d.ts +2 -2
  92. package/dist/plugins/rules/index.js +15 -10
  93. package/dist/plugins/soft-delete/index.d.ts +3 -3
  94. package/dist/plugins/soft-delete/index.js +14 -5
  95. package/dist/plugins/tenant/index.d.ts +2 -2
  96. package/dist/plugins/tenant/index.js +38 -20
  97. package/dist/plugins/timestamps/index.d.ts +1 -1
  98. package/dist/plugins/timestamps/index.js +4 -3
  99. package/dist/plugins/validation/index.d.ts +2 -2
  100. package/dist/plugins/validation/index.js +26 -6
  101. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  102. package/dist/postgres/index.d.ts +5 -5
  103. package/dist/postgres/index.js +20 -11
  104. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  105. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  106. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  107. package/dist/query/index.d.ts +2 -2
  108. package/dist/query/index.js +3 -3
  109. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  110. package/dist/queue-BYUCjr59.d.ts +252 -0
  111. package/dist/react/index.d.ts +8 -7
  112. package/dist/react/index.js +4 -3
  113. package/dist/react/server.d.ts +14 -3
  114. package/dist/react/server.js +10 -2
  115. package/dist/react/session.d.ts +7 -1
  116. package/dist/react/session.js +9 -1
  117. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  118. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  119. package/dist/realtime/index.d.ts +3 -3
  120. package/dist/realtime/index.js +5 -4
  121. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-BTrQ_ZTp.d.ts} +1 -1
  122. package/dist/{resolve-BAqQcwMW.d.ts → resolve-C15TJq77.d.ts} +11 -5
  123. package/dist/{resource-My_y-Syh.js → resource-CRxxwkQq.js} +22 -7
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-D8iqWMr-.d.ts} +5 -5
  125. package/dist/respond-D2OQl62-.d.ts +371 -0
  126. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  127. package/dist/server/index.d.ts +14 -12
  128. package/dist/server/index.js +11 -17
  129. package/dist/{server-BA75B0mT.js → server-Csx2sRjg.js} +251 -34
  130. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  131. package/dist/signers-Cc6r98SW.d.ts +162 -0
  132. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  133. package/dist/sql/index.d.ts +247 -10
  134. package/dist/sql/index.js +8590 -779
  135. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  136. package/dist/ssr/index.d.ts +4 -4
  137. package/dist/ssr/index.js +1 -2
  138. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  139. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  140. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  141. package/dist/storage/index.d.ts +4 -4
  142. package/dist/storage/index.js +155 -26
  143. package/dist/support-DT-OhSxD.d.ts +88 -0
  144. package/dist/support-Dk6vdmEw.js +291 -0
  145. package/dist/support-view-Da5peKFg.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  148. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  149. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  150. package/dist/testing/index.d.ts +100 -10
  151. package/dist/testing/index.js +336 -8
  152. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  153. package/dist/user-change-CHFEUro1.js +49 -0
  154. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  155. package/dist/{version-Bmf7_FzM.js → version-DM_Roffb.js} +1 -1
  156. package/dist/{view-B64o81wp.d.ts → view-BEf0diC5.d.ts} +8 -3
  157. package/dist/{mfa-byfjruR9.js → view-Bq64ki8-.js} +95 -38
  158. package/dist/webhooks/index.d.ts +135 -4
  159. package/dist/webhooks/index.js +588 -2
  160. package/dist/wire-D2dT86vi.js +35 -0
  161. package/package.json +16 -8
  162. package/schemas/config-v1.json +131 -1
  163. package/schemas/doctor-report-v1.json +10 -0
  164. package/skills/better-supabase/SKILL.md +9 -6
  165. package/skills/better-supabase/references/plugins.md +6 -3
  166. package/skills/better-supabase/references/troubleshooting.md +1 -0
  167. package/skills/better-supabase-api/SKILL.md +9 -2
  168. package/skills/better-supabase-api/references/adapters.md +11 -8
  169. package/skills/better-supabase-auth/SKILL.md +3 -0
  170. package/skills/better-supabase-auth/references/permdock.md +56 -11
  171. package/dist/cli/gen-D5cLCULx.js +0 -2
  172. package/dist/cli/sql-D4VbqmXi.js +0 -199
  173. package/dist/events-B21UOhvP.d.ts +0 -84
  174. package/dist/executor-B5IoUxLE.d.ts +0 -195
  175. package/dist/invalidate-CgxmRamI.js +0 -24
  176. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  177. package/dist/plugin-BqR6wKMB.js +0 -23
  178. package/dist/respond-DgIbdrt8.d.ts +0 -194
  179. package/dist/view-BrJI2LKp.js +0 -45
@@ -292,7 +292,10 @@
292
292
  "type": "object",
293
293
  "additionalProperties": false,
294
294
  "properties": {
295
- "scope": { "type": "string", "default": "organization" }
295
+ "scope": {
296
+ "type": "string",
297
+ "description": "The PermDock scope tenants are. Defaults to the manifest's root scope, the rls.scopes entry without within."
298
+ }
296
299
  }
297
300
  }
298
301
  ]
@@ -346,6 +349,11 @@
346
349
  "description": "Tables that broadcast change signals (`table` or `schema.table`). Install with `better-supabase sql add realtime-tables`.",
347
350
  "type": "array",
348
351
  "items": { "type": "string" }
352
+ },
353
+ "global": {
354
+ "description": "Tables in `tables` without a tenant column, which broadcast on one topic for every signed-in user.",
355
+ "type": "array",
356
+ "items": { "type": "string" }
349
357
  }
350
358
  }
351
359
  },
@@ -359,6 +367,14 @@
359
367
  "kit": { "type": "array", "items": { "type": "string" } }
360
368
  }
361
369
  },
370
+ "kits": {
371
+ "type": "object",
372
+ "description": "How each SQL kit module maps onto the database, keyed by module name.",
373
+ "properties": {
374
+ "access": { "$ref": "#/$defs/accessKit" }
375
+ },
376
+ "additionalProperties": { "$ref": "#/$defs/kitModule" }
377
+ },
362
378
  "seed": {
363
379
  "type": "object",
364
380
  "additionalProperties": false,
@@ -394,6 +410,120 @@
394
410
  }
395
411
  },
396
412
  "$defs": {
413
+ "stringRecord": {
414
+ "type": "object",
415
+ "additionalProperties": { "type": "string" }
416
+ },
417
+ "kitModuleProperties": {
418
+ "mode": { "enum": ["managed", "adopt", "custom"], "default": "managed" },
419
+ "schema": { "type": "string", "default": "better_supabase" },
420
+ "tables": {
421
+ "type": "object",
422
+ "additionalProperties": { "type": ["string", "null"] }
423
+ },
424
+ "columns": {
425
+ "type": "object",
426
+ "additionalProperties": {
427
+ "type": "object",
428
+ "additionalProperties": { "type": ["string", "null"] }
429
+ }
430
+ },
431
+ "idType": { "type": "string" },
432
+ "triggerPrefix": { "type": "string", "default": "bs_" },
433
+ "permissions": { "$ref": "#/$defs/stringRecord" },
434
+ "options": { "type": "object" }
435
+ },
436
+ "kitModule": {
437
+ "type": "object",
438
+ "additionalProperties": false,
439
+ "properties": {
440
+ "mode": { "$ref": "#/$defs/kitModuleProperties/mode" },
441
+ "schema": { "$ref": "#/$defs/kitModuleProperties/schema" },
442
+ "tables": { "$ref": "#/$defs/kitModuleProperties/tables" },
443
+ "columns": { "$ref": "#/$defs/kitModuleProperties/columns" },
444
+ "idType": { "$ref": "#/$defs/kitModuleProperties/idType" },
445
+ "triggerPrefix": {
446
+ "$ref": "#/$defs/kitModuleProperties/triggerPrefix"
447
+ },
448
+ "permissions": { "$ref": "#/$defs/kitModuleProperties/permissions" },
449
+ "options": { "$ref": "#/$defs/kitModuleProperties/options" }
450
+ }
451
+ },
452
+ "accessKit": {
453
+ "type": "object",
454
+ "additionalProperties": false,
455
+ "properties": {
456
+ "mode": { "$ref": "#/$defs/kitModuleProperties/mode" },
457
+ "schema": { "$ref": "#/$defs/kitModuleProperties/schema" },
458
+ "tables": { "$ref": "#/$defs/kitModuleProperties/tables" },
459
+ "columns": { "$ref": "#/$defs/kitModuleProperties/columns" },
460
+ "idType": { "$ref": "#/$defs/kitModuleProperties/idType" },
461
+ "triggerPrefix": {
462
+ "$ref": "#/$defs/kitModuleProperties/triggerPrefix"
463
+ },
464
+ "permissions": { "$ref": "#/$defs/kitModuleProperties/permissions" },
465
+ "options": { "$ref": "#/$defs/kitModuleProperties/options" },
466
+ "model": {
467
+ "enum": ["roles", "catalog", "permdock", "custom"],
468
+ "default": "roles"
469
+ },
470
+ "roles": {
471
+ "type": "object",
472
+ "additionalProperties": {
473
+ "type": "array",
474
+ "items": { "type": "string" }
475
+ }
476
+ },
477
+ "functions": {
478
+ "type": "object",
479
+ "additionalProperties": false,
480
+ "properties": {
481
+ "can": { "type": "string" },
482
+ "tenantIdsWith": { "type": "string" },
483
+ "isPlatform": { "type": "string" },
484
+ "canUser": { "type": "string" },
485
+ "canAssign": { "type": "string" },
486
+ "permissionClaims": { "type": "string" }
487
+ }
488
+ },
489
+ "platformClaim": {
490
+ "type": "string",
491
+ "default": "platform_permissions"
492
+ },
493
+ "disabled": {
494
+ "type": "object",
495
+ "additionalProperties": false,
496
+ "properties": {
497
+ "tenant": { "type": "string" },
498
+ "tenantKey": { "type": "string" },
499
+ "user": { "type": "string" },
500
+ "userKey": { "type": "string" }
501
+ }
502
+ },
503
+ "activeTenant": {
504
+ "oneOf": [
505
+ { "enum": ["claim", "resolver"] },
506
+ {
507
+ "type": "object",
508
+ "additionalProperties": false,
509
+ "required": ["profileColumn"],
510
+ "properties": {
511
+ "profileColumn": { "type": "string" },
512
+ "key": { "type": "string" }
513
+ }
514
+ }
515
+ ]
516
+ },
517
+ "permdock": {
518
+ "type": "object",
519
+ "additionalProperties": false,
520
+ "properties": {
521
+ "schema": { "type": "string" },
522
+ "scope": { "type": "string" }
523
+ }
524
+ }
525
+ }
526
+ },
397
527
  "casing": { "enum": ["snake", "camel"] },
398
528
  "privileges": {
399
529
  "type": "array",
@@ -73,6 +73,14 @@
73
73
  "BS304",
74
74
  "BS305",
75
75
  "BS306",
76
+ "BS307",
77
+ "BS308",
78
+ "BS309",
79
+ "BS310",
80
+ "BS311",
81
+ "BS312",
82
+ "BS313",
83
+ "BS314",
76
84
  "BS401",
77
85
  "BS402",
78
86
  "BS403",
@@ -81,6 +89,8 @@
81
89
  "BS406",
82
90
  "BS407",
83
91
  "BS408",
92
+ "BS409",
93
+ "BS410",
84
94
  "BS501",
85
95
  "BS502"
86
96
  ]
@@ -53,16 +53,19 @@ stay hidden.
53
53
 
54
54
  ## Workflow: after upgrading better-supabase
55
55
 
56
- 1. Rerun `pnpm better-supabase sql add` with the kit modules the project
57
- installed, so their SQL gets the release's fixes, then write a migration
58
- from the changed files.
59
- 2. Run `pnpm better-supabase gen` and commit the result. Since 0.3, a
56
+ 1. Run `pnpm better-supabase codemod <version> --dry-run` for each minor
57
+ version you crossed (`codemod` without a name lists them), then without
58
+ `--dry-run`, and fix the lines it lists for review.
59
+ 2. Run `pnpm better-supabase sql upgrade`, so the kit modules get the
60
+ release's SQL and any forward steps land in a migration, then write a
61
+ migration from the changed files.
62
+ 3. Run `pnpm better-supabase gen` and commit the result. Since 0.3, a
60
63
  composite foreign key such as `(customer_id, organization_id)` is named
61
64
  after the remaining column (`customer`); set `tables.<name>.relations` in
62
65
  the config to keep an old relation name.
63
- 3. Run `pnpm better-supabase skills install --check` and reinstall the skills
66
+ 4. Run `pnpm better-supabase skills install --check` and reinstall the skills
64
67
  when it reports them stale.
65
- 4. Read the release's breaking changes in
68
+ 5. Read the release's breaking changes in
66
69
  https://github.com/ScaleDockHQ/better-supabase/blob/main/CHANGELOG.md
67
70
  (guides live under https://bettersupabase.com/docs/migration). For 0.3: time values are
68
71
  `Temporal` (see below), `--db-url` is gone, `doctor --format json` is
@@ -53,9 +53,12 @@ filled it.
53
53
  RLS still decides what a user can read and write. `tenant()` makes queries
54
54
  explicit and fills the column; it doesn't replace the policy.
55
55
 
56
- The tenant comes from the verified `tenant_id` claim (renamed with
57
- `claims.tenant` in `better-supabase.config.ts`), then `app_metadata.tenant_id`.
58
- Never read it from `user_metadata`, a URL or a request body. When the project
56
+ The tenant comes from `context.tenant`, which the server's `tenant` resolver
57
+ sets per request (the default `kits.access.activeTenant: 'resolver'`), then
58
+ the verified `tenant_id` claim (renamed with `claims.tenant` in
59
+ `better-supabase.config.ts`), then `app_metadata.tenant_id`. A resolver may
60
+ read the URL, but `current_tenant_id()` only returns a tenant the caller is a
61
+ member of. Never trust a tenant from `user_metadata` or a request body. When the project
59
62
  has a `permdock.config.ts`, PermDock's hook writes that claim and the
60
63
  memberships: don't run `sql add tenant`, and follow the PermDock reference of
61
64
  the `better-supabase-auth` skill
@@ -16,6 +16,7 @@ Branch on `result.error.kind`, not on the message.
16
16
  | `foreign_key` | Referenced row missing, or still referenced on delete | Create the parent first, or delete children |
17
17
  | `not_null`, `check`, `exclusion` | A constraint rejected the row | Validate input earlier with the `validation()` plugin |
18
18
  | `invalid_input` | Postgres couldn't parse a value (bad uuid, enum) | Validate before the call |
19
+ | `invalid_value` | A stored value the app type can't hold (`infinity` in a Temporal timestamp) | Store a real timestamp or `null`, or read the column without the codec |
19
20
  | `validation` | A Standard Schema rejected the input; see `issues` | Show the issues to the user |
20
21
  | `invalid_request` | The query can't run over PostgREST (for example a relation filter on `update`) | Read the keys first, or use a database function or `better-supabase/postgres` |
21
22
  | `multiple_rows` | `findUnique` or a single-row write matched more than one row | Filter by a unique key |
@@ -88,11 +88,18 @@ for lists that clients read page by page: the list takes `after` instead of
88
88
 
89
89
  ## Background work (`better-supabase/jobs`, needs `sql add jobs idempotency webhook-inbox`)
90
90
 
91
- - `createJobs(postgres.admin, { queue_name: zodSchema })` runs on Supabase Queues (pgmq): `enqueue` in the request, then `work` or `drain` in a worker. The handler throws to retry. `schedule(name, cron, queue, payload)` uses pg_cron. Queue names are lowercase letters, digits and underscores. With a service-role Supabase client instead of SQL, it uses the `pgmq_public` RPCs (no dedupe or schedules).
91
+ - `createJobs(postgres.admin, { queue_name: zodSchema })` runs on Supabase Queues (pgmq): `enqueue` in the request, then `work` or `drain` in a worker. The handler throws to retry. `schedule(name, cron, queue, payload, { timeZone })` uses pg_cron, or with `kits.jobs.options.scheduler: "drain"` a `jobs.drainRoute({ secret: process.env.CRON_SECRET, handlers })` route that Vercel Cron calls; `kits.jobs.options.backend: "table"` runs without pgmq. Queue names are lowercase letters, digits and underscores. With a service-role Supabase client instead of SQL, it uses the `pgmq_public` RPCs (no dedupe or schedules).
92
92
  - `createIdempotency(postgres.admin).handle(request, handler)` for POST endpoints that clients retry.
93
93
  - `createInbox(postgres.admin, { source, secrets }).receive(request)` for webhooks; `process(handler)` later.
94
94
  - Times are `Temporal.Instant`: `EnqueueOptions.runAt`, `Job.enqueuedAt`, `Job.visibleUntil`, `InboxMessage.receivedAt` and webhook timestamps.
95
- - Schedule cleanup with pg_cron at a quiet hour: `better_supabase.purge_job_archive('<queue>')`, `purge_webhooks()`, `purge_audit_log()` and `purge_idempotency_keys()`. Only `service_role` can execute them.
95
+ - Schedule cleanup with pg_cron at a quiet hour: `better_supabase.purge_job_archive('<queue>')`, `purge_webhooks()`, `purge_audit_log()`, `purge_idempotency_keys()`, `purge_rate_limits()`, `purge_outbox()`, `purge_notifications()` and `purge_webhook_deliveries()`, for the modules you installed. Only `service_role` can execute them.
96
+ - Audit a table with `better_supabase.audit('public.t', redact => '{secret}', event_prefix => 't')`; record non-row events with `better_supabase.audit_event(event_type, ...)` and an `idempotency_key`. For per-tenant retention, write an `audit_retention(tenant)` SQL function or call `purgeAuditLog(sql, { retention })` from `better-supabase/jobs`.
97
+ - For a "view as user" support mode, add the `support-sessions` SQL module, pass `support: supportSessions({ store: sqlSupportStore(postgres) })` to `createServer`, call `bs.startSupport({ targetUserId, reason })` and `bs.stopSupport()` from server actions, and show a banner with `useSupportSession()`. Sessions are read-only by default; grant admins the `support.start` platform permission.
98
+ - For teams, add the `organizations` and `invitations` SQL modules and call them through `createOrgs({ transport: sqlTransport(postgres.asUser(claims)), events: betterSupabase.events, onInvite })` from `better-supabase/orgs`. Send the invitation email in `onInvite`, show `previewInvitation(token)` on the accept page, and switch with `orgs.switch(id)`, refreshing the session when it returns `refresh: true`. Errors carry codes such as `ORG_SLUG_TAKEN` in `hint`. Existing tables work with `mode: 'adopt'`.
99
+ - For user profiles, add the `profiles` SQL module: sign-up creates the row from auth metadata with a unique username and mirrors the email; users update only the `updatable` columns. Adopt an existing table with `mode: 'adopt'` and `syncTrigger: false` when your own trigger creates profiles. Use `avatarBucket()` and `orgLogoBucket()` from `better-supabase/storage` for uploads; `policy: { access: { read, write } }` checks any bucket through the access contract.
100
+ - For events other systems must see, add the `outbox` SQL module and write events with `emit_event(type, payload, subject, tenant, key)` inside the same transaction as the change (the kit modules emit theirs automatically). Relay them with `createOutbox(postgres.admin, { source }).relayRoute({ secret, consumers: { name: httpSink(url) } })` from `better-supabase/jobs` after `outbox.register(name, { types })`. Delivery is at least once; receivers deduplicate on the CloudEvent `id`.
101
+ - For in-app notifications, add the `notifications` SQL module and send with `createNotifications({ transport: sqlTransport(postgres.asUser(claims)), types })` from `better-supabase/notifications`, where `types` maps each notification type to a Standard Schema for its data. Pass a `key` so retries don't notify twice. Show them with `useNotifications({ topic, load })` from `better-supabase/react`; the topic defaults to `notifications:{userId}`. For email or push, pass `channels` and call `notifications.deliver()` from a cron route with a service connection.
102
+ - For webhooks to customers' endpoints, add the `webhooks-out` SQL module and publish with `createWebhooks({ transport: sqlTransport(postgres.admin) }).publish({ type, data, tenant, id })` from `better-supabase/webhooks`. Pass an `id` so retries don't send twice. Send with `webhooks.deliverRoute({ secret: process.env.CRON_SECRET })` from a cron route; deliveries are signed with Standard Webhooks and only go to public HTTPS URLs by default.
96
103
 
97
104
  Done when a failing job is retried and then archived after `maxAttempts`,
98
105
  a replayed webhook or POST doesn't run twice, and each kit table has a purge
@@ -15,6 +15,7 @@ export const betterSupabase = defineSupabase(schema);
15
15
  ## Next.js
16
16
 
17
17
  ```ts title="src/lib/supabase/server.ts"
18
+ import "server-only";
18
19
  import { createNext } from "better-supabase/next";
19
20
 
20
21
  import { betterSupabase } from "./index";
@@ -44,15 +45,15 @@ export const GET = bs.route(
44
45
  ## Hono
45
46
 
46
47
  ```ts title="src/server.ts"
47
- import { type HonoEnv, createHono } from "better-supabase/hono";
48
- import { Hono } from "hono";
48
+ import { createHono } from "better-supabase/hono";
49
49
 
50
- import { type Functions, type Models, betterSupabase } from "./lib/supabase";
50
+ import { betterSupabase } from "./lib/supabase";
51
51
 
52
52
  const bs = createHono(betterSupabase);
53
53
 
54
- const app = new Hono<HonoEnv<Models, Functions, unknown>>()
55
- .onError(bs.onError)
54
+ // bs.app() is new Hono<typeof bs.Env>() with bs.onError installed.
55
+ const app = bs
56
+ .app()
56
57
  .use("/api/*", bs.middleware())
57
58
  .get("/api/me", (c) => c.json({ kind: c.var.auth.kind }))
58
59
  .route(
@@ -106,7 +107,7 @@ import { betterSupabase } from "../_shared/supabase.ts";
106
107
  const bs = createMcp(betterSupabase, {
107
108
  name: "crm",
108
109
  version: "0.1.0",
109
- scopes: ["openid", "crm.read"],
110
+ advertisedScopes: ["openid", "crm.read"],
110
111
  allowedOrigins: ["https://claude.ai"],
111
112
  allowedHosts: ["crm.example.com", "localhost"],
112
113
  resourceDocumentation: "https://crm.example.com/docs/mcp",
@@ -126,6 +127,8 @@ const bs = createMcp(betterSupabase, {
126
127
  Deno.serve(bs.fetch);
127
128
  ```
128
129
 
129
- Tools run as the calling user, so RLS applies to every tool call. `scopes`
130
- on `createMcp` only advertises scopes; `authorize` refuses the call. A
130
+ Tools run as the calling user, so RLS applies to every tool call.
131
+ `advertisedScopes` only advertises scopes; `requiredScopes` refuses delegated
132
+ tokens that lack one, and `authorize` refuses a single call. The session comes
133
+ from the `Authorization` header only, never from a cookie. A
131
134
  `delegation` that is unset means the user's own token, which no scope limits.
@@ -106,6 +106,9 @@ membership lasts until the next refresh. When that window matters:
106
106
  - Write policies that read the source of truth, for example
107
107
  `organization_id in (select better_supabase.member_org_ids())`, instead of
108
108
  the claim.
109
+ - For signed-out, banned or deleted users, add the `sessions` SQL module and a
110
+ restrictive policy with `using ((select better_supabase.session_active()))`
111
+ on the tables that guard sensitive data.
109
112
  - After a change the user made themselves (joining an organization), call
110
113
  `supabase.auth.refreshSession()` in the browser.
111
114
  - In Next.js, drop the user's cached views with
@@ -26,29 +26,74 @@ the other.
26
26
  `hasEntitlement(session, ...)` works.
27
27
  - Run `permdock supabase inspect --out` before `sql add entitlements`. With
28
28
  `permdock.manifest.json` present, the module reads PermDock's
29
- `member_<scope>_ids` helpers and doesn't add `tenant`. Set
30
- `entitlements.permdock: { scope }` in `better-supabase.config.ts` for a
31
- scope other than `organization`.
29
+ `member_<scope>_ids` helpers and doesn't add `tenant`. The scope is the
30
+ manifest's root scope (the `rls.scopes` entry without `within`); set
31
+ `entitlements.permdock: { scope }` in `better-supabase.config.ts` for
32
+ another one.
32
33
  - Name the same tenant claim on both sides. A non-default `claims.tenant` in
33
34
  `better-supabase.config.ts` must also be PermDock's `rls.tenantClaim` and
34
35
  go to `subjectFromSupabase` or `subjectFromSupabaseSession` as `{ tenant }`.
35
36
  - Validate claims with PermDock's schema: `betterSupabase.claims(supabaseClaims().extend(appClaims))`.
36
- - The helpers live in PermDock's `rls.schema`, which must equal the `schema`
37
- option of buckets and topics in `permdock` mode.
37
+ - The helpers live in PermDock's `rls.schema` (`permdock` by default). Buckets
38
+ and topics in `permdock` mode call them in `permdock` unless their `schema`
39
+ option names another schema; it must equal `rls.schema`.
40
+ - Use the scope names the manifest declares (`organization`, not PermDock's
41
+ `tenant` alias): the helpers are named after them, and doctor reports a
42
+ key checked at another scope than its catalog entry.
43
+ - The entitlements module needs PermDock's hook to fill `claims.features`
44
+ from `better_supabase.feature_claims`
45
+ (`supabase.hook.claims: { features: 'better_supabase.feature_claims' }`).
38
46
  - The `permdock` policy mode checks role and scope only. Use it only for
39
47
  permissions whose `rowConditions` is `false` in `permissions.catalog.json`.
48
+ A missing flag or a key the catalog doesn't list is unknown and refused;
49
+ regenerate the catalog with a current `permdock catalog`.
40
50
  Leave permissions with row conditions to the policies
41
51
  `permdock rls generate` writes; doctor reports BS214 and `permdock doctor`
42
52
  PD037 otherwise.
43
53
  - `permdock` mode compares ids as text, so a path or topic segment must be
44
54
  the id's lowercase form.
45
55
  - In MCP servers, narrow `tool.meta` with PermDock's `isPermission`, answer
46
- `visible` with `mayUse` and `authorize` with `can`. `can` decides without
47
- a row, so check row-conditioned permissions inside `run` or rely on RLS.
48
- - In Next.js, cache the permission snapshot with
49
- `bs.cached({ tags: [snapshotTag(userId)], life: cacheLifeFor(snapshot) })`
50
- and drop it with `bs.invalidateSession(userId, { tags: [snapshotTag(userId)] })`
51
- when a role or plan changes.
56
+ `visible` with `mayUse` and `authorize` with `can`. A tool without a
57
+ PermDock permission in `meta` is denied: `visible` returns `false` and
58
+ `authorize` returns `{ allowed: false }`. Use `decide` instead of `can` to
59
+ put the denial reason in the refusal. `can` decides without a row, so check
60
+ row-conditioned permissions inside `run` or rely on RLS.
61
+ - In oRPC and Hono, pass the adapter's auth state to PermDock:
62
+ `createPermDock(policy, { subject: ({ context }) => subjectFromSupabaseSession(context.auth) })`
63
+ from `permdock/orpc`, or `(c) => subjectFromSupabaseSession(c.get("auth"))`
64
+ from `permdock/hono`. Fail closed: export one builder that takes a
65
+ permission (`(permission) => authed.use(pd.protect(permission))`, or the
66
+ route helper `[bs.middleware(), pd.protect(permission)]` in Hono), and don't
67
+ install `bs.middleware()` app-wide, so a route without a permission has no
68
+ `db`.
69
+ - In Next.js, cache the permission snapshot in a `'use cache: private'`
70
+ loader. The user id and the snapshot only exist after `bs.cached()`
71
+ returns, so pass it the static tags only, then tag and time the entry from
72
+ inside the loader with Next.js's `cacheTag` and `cacheLife`:
73
+
74
+ ```ts
75
+ import { cacheLife, cacheTag } from "next/cache";
76
+ import { snapshotFor } from "permdock";
77
+ import { cacheLifeFor, snapshotTag } from "permdock/next";
78
+ import { subjectFromSupabaseSession } from "permdock/supabase";
79
+
80
+ export async function loadSnapshot(orgId: string) {
81
+ "use cache: private";
82
+ const { session } = await bs.cached({ tags: [`org:${orgId}`] });
83
+ const snapshot = snapshotFor(policy, subjectFromSupabaseSession(session), {
84
+ tenant: orgId,
85
+ });
86
+ if (session.kind === "user") cacheTag(snapshotTag(session.user.id));
87
+ cacheLife(cacheLifeFor(snapshot));
88
+ return snapshot;
89
+ }
90
+ ```
91
+
92
+ `bs.cached()` already caps the stale time at the token's expiry, and Next.js
93
+ keeps the smaller `stale` of the two `cacheLife` calls. When a role or plan
94
+ changes, drop the user's entries and the snapshot with
95
+ `bs.invalidateSession(userId, { tags: [snapshotTag(userId)] })`.
96
+
52
97
  - Doctor BS405 measures `memberships` plus `attrs` against PermDock's 1 KB
53
98
  budget and the whole token against 2 KB; run `doctor --as <user id>`.
54
99
 
@@ -1,2 +0,0 @@
1
- import { n as genCommand } from "./gen-Ccn_p90L.js";
2
- export { genCommand };
@@ -1,199 +0,0 @@
1
- import { r as defineCliCommand } from "./command-CK14fP91.js";
2
- import { r as writeIfChanged, t as display } from "./io-k41_z-jT.js";
3
- import { i as plain, r as painter } from "./style-C0NFdJRL.js";
4
- import { d as readSupabaseToml, f as schemaPaths, l as migrationCommand } from "./config-D9R0o1LH.js";
5
- import { c as readPermdock, l as fileDiff, o as permdockConfig, r as entitlementsMode, t as compiledReadSets } from "./read-sets-CMa29t9a.js";
6
- import { resolve } from "node:path";
7
- import { existsSync } from "node:fs";
8
- import { readFile } from "node:fs/promises";
9
- import { SQL_MODULES, kitLayout, renderKit, resolveModules, sameKitFile } from "better-supabase/sql";
10
- //#region src/cli/commands/sql.ts
11
- const SQL_ARGS = {
12
- action: {
13
- type: "positional",
14
- required: false,
15
- description: "list (modules and whether they are installed), add <module...>, sync or print <module>"
16
- },
17
- check: {
18
- type: "boolean",
19
- description: "With sync: fail when a file is stale"
20
- },
21
- "tests-dir": {
22
- type: "string",
23
- description: "Where the pgtap module goes. Defaults to sql.testsDir",
24
- valueHint: "dir"
25
- },
26
- "dry-run": {
27
- type: "boolean",
28
- description: "Show what would be written"
29
- },
30
- force: {
31
- type: "boolean",
32
- description: "Write tenant even though a permdock.config.ts is present"
33
- }
34
- };
35
- const USAGE = "Run `better-supabase sql --help` for the actions.";
36
- /** Modules that fill a claim PermDock's hook also writes (`memberships`). */
37
- const PERMDOCK_OWNED = /* @__PURE__ */ new Set(["tenant"]);
38
- /** PermDock's helpers for the `entitlements` module; throws for a scope the manifest lacks. */
39
- async function permdockFor(config) {
40
- const mode = entitlementsMode(config, await readPermdock(config.root, config.permdock));
41
- switch (mode.kind) {
42
- case "tenant": return;
43
- case "permdock": return mode.permdock;
44
- case "invalid": throw new TypeError(mode.problem);
45
- default: return mode;
46
- }
47
- }
48
- async function layout(config, args) {
49
- return kitLayout(config, args["tests-dir"], [], await permdockFor(config));
50
- }
51
- /** The layout, with `config.readSets` compiled when `names` includes `read-sets`. */
52
- async function layoutFor(config, args, names) {
53
- const permdock = await permdockFor(config);
54
- const needsReadSets = resolveModules(names, { ...permdock ? { permdock } : {} }).some((module) => module.name === "read-sets");
55
- return kitLayout(config, args["tests-dir"], needsReadSets ? await compiledReadSets(config) : [], permdock);
56
- }
57
- async function write(config, args, names, kit) {
58
- const lines = [];
59
- const dryRun = args["dry-run"] === true;
60
- for (const file of renderKit(names, kit)) {
61
- const path = resolve(config.root, file.path);
62
- const shown = display(config.root, file.path);
63
- if (dryRun) {
64
- lines.push(`Would write ${shown} (${file.module})`);
65
- continue;
66
- }
67
- const wrote = await writeIfChanged(path, file.contents);
68
- lines.push(`${wrote ? "Wrote" : "Unchanged"} ${shown} (${file.module})`);
69
- }
70
- return lines;
71
- }
72
- /** A note when migra's `schema_paths` is set and misses kit files, which `supabase db diff` would then skip. */
73
- async function unlistedKitFiles(config, names, kit) {
74
- const order = await schemaPaths(config.root, await readSupabaseToml(config.root));
75
- if (!order.configured) return [];
76
- const unlisted = new Set(order.unlisted);
77
- const listed = new Set(order.files.filter((path) => !unlisted.has(path)));
78
- const dir = `${config.sql.dir.replace(/\/$/, "")}/`;
79
- const missing = renderKit(names, kit).map((file) => file.path).filter((path) => path.startsWith(dir) && !listed.has(path));
80
- if (missing.length === 0) return [];
81
- return [
82
- "",
83
- "supabase/config.toml sets [db.migrations] schema_paths, and no entry matches these files, so `supabase db diff` skips them.",
84
- "Add them before the schemas that call their functions:",
85
- ...missing.map((path) => ` "./${path.startsWith("supabase/") ? path.slice(9) : path}",`)
86
- ];
87
- }
88
- async function runSql(config, args, paint = plain) {
89
- const [action, ...names] = args._;
90
- switch (action) {
91
- case "list": {
92
- const kit = await layout(config, args);
93
- const files = new Map(renderKit(Object.keys(SQL_MODULES), kit).map((file) => [file.module, file.path]));
94
- return {
95
- code: 0,
96
- output: `${Object.values(SQL_MODULES).map((module) => {
97
- const path = files.get(module.name);
98
- const installed = existsSync(resolve(config.root, path));
99
- const tracked = config.sql.kit.includes(module.name);
100
- const mark = installed ? tracked ? "●" : "○" : " ";
101
- const requires = kit.permdock && module.permdockRequires ? module.permdockRequires : module.requires;
102
- const needs = requires.length > 0 ? ` (needs ${requires.join(", ")})` : "";
103
- return `${mark} ${module.name.padEnd(15)} ${module.description}${needs}`;
104
- }).join("\n")}\n\n● installed and in sql.kit ○ installed, not in sql.kit`
105
- };
106
- }
107
- case "add": {
108
- if (names.length === 0) return {
109
- code: 2,
110
- error: `Name at least one module.\n${USAGE}`
111
- };
112
- const unknown = names.filter((name) => !(name in SQL_MODULES));
113
- if (unknown.length > 0) return {
114
- code: 2,
115
- error: `Unknown module ${unknown.join(", ")}. Available: ${Object.keys(SQL_MODULES).join(", ")}`
116
- };
117
- const permdock = permdockConfig(config.root);
118
- const hookModules = names.filter((name) => PERMDOCK_OWNED.has(name));
119
- if (permdock && hookModules.length > 0 && args.force !== true) return {
120
- code: 1,
121
- error: [
122
- `${permdock} is present, so PermDock owns the access token hook and the memberships claim.`,
123
- `${hookModules.join(" and ")} would add a second source for them. Use \`permdock supabase hook generate\` instead,`,
124
- "or pass --force to write the modules anyway."
125
- ].join("\n")
126
- };
127
- const kit = await layoutFor(config, args, names);
128
- const lines = await write(config, args, names, kit);
129
- const pulledIn = resolveModules(names, kit).map((module) => module.name).filter((name) => PERMDOCK_OWNED.has(name) && !names.includes(name));
130
- if (permdock && pulledIn.length > 0) lines.push("", `${pulledIn.join(" and ")} came along as a dependency: its memberships table backs has_org_role() and has_entitlement().`, `PermDock's hook still owns the memberships claim, so don't call better_supabase.membership_claims from a hook.`, "List better_supabase.memberships as a PermDock membership source if both should agree.");
131
- const untracked = resolveModules(names, kit).map((module) => module.name).filter((name) => !config.sql.kit.includes(name));
132
- if (untracked.length > 0) lines.push("", `Add them to your config so \`sql sync --check\` keeps them current:`, ` sql: { kit: [${[...config.sql.kit, ...untracked].map((name) => `'${name}'`).join(", ")}] }`);
133
- if (args["dry-run"] !== true) lines.push(...await unlistedKitFiles(config, names, kit));
134
- lines.push("", `Then create a migration: ${migrationCommand(await readSupabaseToml(config.root), "better_supabase_kit")}`);
135
- return {
136
- code: 0,
137
- output: lines.join("\n")
138
- };
139
- }
140
- case "sync": {
141
- if (config.sql.kit.length === 0) return {
142
- code: 0,
143
- output: "sql.kit is empty; nothing to sync."
144
- };
145
- if (args.check !== true) return {
146
- code: 0,
147
- output: (await write(config, args, config.sql.kit, await layoutFor(config, args, config.sql.kit))).join("\n")
148
- };
149
- const stale = [];
150
- const diffs = [];
151
- for (const file of renderKit(config.sql.kit, await layoutFor(config, args, config.sql.kit))) {
152
- const current = await readFile(resolve(config.root, file.path), "utf8").catch(() => void 0);
153
- if (!sameKitFile(current, file.contents)) {
154
- const shown = display(config.root, file.path);
155
- stale.push(shown);
156
- diffs.push(fileDiff(shown, current, file.contents, paint));
157
- }
158
- }
159
- return stale.length === 0 ? {
160
- code: 0,
161
- output: "SQL kit files are up to date."
162
- } : {
163
- code: 1,
164
- output: diffs.join("\n\n"),
165
- error: `Out of date: ${stale.join(", ")}. Run \`better-supabase sql sync\`.`
166
- };
167
- }
168
- case "print": {
169
- const [name] = names;
170
- const module = name ? SQL_MODULES[name] : void 0;
171
- if (!module) return {
172
- code: 2,
173
- error: `Name one module: ${Object.keys(SQL_MODULES).join(", ")}`
174
- };
175
- return {
176
- code: 0,
177
- output: module.render ? module.render(config.claims, await layout(config, args)) : module.sql
178
- };
179
- }
180
- case void 0: return {
181
- code: 2,
182
- error: `Name an action.\n${USAGE}`
183
- };
184
- default: return {
185
- code: 2,
186
- error: `Unknown sql action "${action}".\n${USAGE}`
187
- };
188
- }
189
- }
190
- const sqlCommand = defineCliCommand({
191
- meta: {
192
- name: "sql",
193
- description: "Lists, adds, syncs and prints SQL kit modules"
194
- },
195
- args: SQL_ARGS,
196
- run: (args, { config, io }) => runSql(config, args, painter(io.color))
197
- });
198
- //#endregion
199
- export { sqlCommand };