better-supabase 0.4.0 → 0.5.1

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 (180) hide show
  1. package/README.md +135 -16
  2. package/dist/actor-DaJjqD5R.d.ts +43 -0
  3. package/dist/channel-FQW-CnGR.d.ts +70 -0
  4. package/dist/cli/bin.js +1 -1
  5. package/dist/cli/codemod-DqV7612m.js +388 -0
  6. package/dist/cli/{config-D9R0o1LH.js → config-CPVpXP5I.js} +78 -8
  7. package/dist/cli/{config-SSKSTUl2.js → config-dtoekM0v.js} +1 -1
  8. package/dist/cli/{read-sets-CMa29t9a.js → diff-SjkO5rNr.js} +1 -220
  9. package/dist/cli/{dist-CFgGknmZ.js → dist-DBPdlPYK.js} +4 -4
  10. package/dist/cli/{dist-B1MvLv-M.js → dist-DreZ24Sz.js} +1 -1
  11. package/dist/cli/{dist-Du7VnEUG.js → dist-xn4wlboc.js} +112 -1
  12. package/dist/cli/{doctor-BOym3mbL.js → doctor-C_A-KEAz.js} +533 -50
  13. package/dist/cli/{env-BZ0uy8XK.js → env-DmKv_G4A.js} +1 -1
  14. package/dist/cli/gen-CIdSQZad.js +2 -0
  15. package/dist/cli/{gen-Ccn_p90L.js → gen-CjElD1Vw.js} +321 -88
  16. package/dist/cli/index.d.ts +1 -1
  17. package/dist/cli/index.js +4 -4
  18. package/dist/cli/{init-ClnxRYBF.js → init-DMa6M7hV.js} +2 -2
  19. package/dist/cli/{introspect-DxqfkVIv.js → introspect-BKlaIu_M.js} +2 -2
  20. package/dist/cli/{openapi-8WHjxiTJ.js → openapi-Cp5Fyta-.js} +1 -1
  21. package/dist/cli/read-sets-VQcPgxU9.js +436 -0
  22. package/dist/cli/{run-CP20LKk_.js → run-Bo_NLT5n.js} +16 -12
  23. package/dist/cli/{seed-5J6eu_Xg.js → seed-CUlqWh-8.js} +1 -1
  24. package/dist/cli/sql-CVx-1O8G.js +373 -0
  25. package/dist/cli/{version-5mW_8sY1.js → version-BCtlgDQx.js} +1 -1
  26. package/dist/client/index.d.ts +1 -1
  27. package/dist/client/index.js +3 -10
  28. package/dist/clone-BfgmLU90.js +21 -0
  29. package/dist/{compiler-DB3aSFOA.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  30. package/dist/config/index.d.ts +4 -4
  31. package/dist/config/index.js +4 -4
  32. package/dist/{config-C1CYcm6o.d.ts → config-Bv7gvfzC.d.ts} +185 -11
  33. package/dist/{config-CbvFmGD8.js → config-DreERlKo.js} +1 -1
  34. package/dist/{define-CRS2MRV3.d.ts → define-Bp_BZa5h.d.ts} +11 -7
  35. package/dist/{define-Bh0AqTik.js → define-cNKzqwL_.js} +202 -16
  36. package/dist/edge/index.d.ts +16 -5
  37. package/dist/edge/index.js +10 -7
  38. package/dist/{entitlements-Dm-FHpNH.d.ts → entitlements-BiJZ4t49.d.ts} +1 -1
  39. package/dist/env/index.d.ts +2 -2
  40. package/dist/env/index.js +34 -1
  41. package/dist/{errors-L0xK41y_.js → errors-DtwXa6b8.js} +1 -0
  42. package/dist/events/index.d.ts +3 -2
  43. package/dist/events/index.js +65 -12
  44. package/dist/events-aIO7vsIk.d.ts +230 -0
  45. package/dist/{result-CqEsbgYs.d.ts → executor-BunPOO-k.d.ts} +200 -1
  46. package/dist/{executor-CZ7ImRiD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  47. package/dist/{executor-CVt6dD0B.js → executor-XycESQPH.js} +3 -3
  48. package/dist/hono/index.d.ts +17 -6
  49. package/dist/hono/index.js +40 -5
  50. package/dist/impersonation-BgSHAYLf.js +133 -0
  51. package/dist/{resolve-BAqQcwMW.d.ts → impersonation-BmePsuxz.d.ts} +36 -6
  52. package/dist/{index-BhHEC04r.d.ts → index-C7f8cOBM.d.ts} +2 -2
  53. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  54. package/dist/{index-Cf-D2jk1.d.ts → index-CWMDdEr4.d.ts} +2 -2
  55. package/dist/{index-D5IfUIDh.d.ts → index-DOESKG27.d.ts} +33 -8
  56. package/dist/{index-DFmOpBGC.d.ts → index-DgILJRXj.d.ts} +35 -9
  57. package/dist/{index-RlLXd8dL.d.ts → index-DmXSxbLi.d.ts} +4 -4
  58. package/dist/{index-BBdvo-5d.d.ts → index-Dp2VDwog.d.ts} +49 -6
  59. package/dist/{index-528Ugmyp.d.ts → index-SaTsEfGP.d.ts} +76 -8
  60. package/dist/{index-BAy4tsAP.d.ts → index-fag8VJZ2.d.ts} +3 -4
  61. package/dist/index.d.ts +30 -13
  62. package/dist/index.js +10 -9
  63. package/dist/jobs/index.d.ts +147 -101
  64. package/dist/jobs/index.js +744 -237
  65. package/dist/{json-schema-DP6V7cbr.js → json-schema-m1fiwKgN.js} +22 -1
  66. package/dist/kit-events-CTFDfSwb.js +60 -0
  67. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  68. package/dist/kit-transport-DJlHQUBU.js +55 -0
  69. package/dist/{config-BpxDfnbg.js → kits-CGLLrNPd.js} +37 -9
  70. package/dist/list/index.d.ts +1 -1
  71. package/dist/{live-BHakSWWo.d.ts → live-D9W2M5aO.d.ts} +6 -5
  72. package/dist/mcp/index.d.ts +25 -8
  73. package/dist/mcp/index.js +23 -14
  74. package/dist/{mfa-C7MNQ1Ob.d.ts → mfa-Cu96oJdw.d.ts} +2 -16
  75. package/dist/next/index.d.ts +31 -10
  76. package/dist/next/index.js +136 -27
  77. package/dist/notifications/index.d.ts +162 -0
  78. package/dist/notifications/index.js +264 -0
  79. package/dist/openapi/index.d.ts +1 -1
  80. package/dist/openapi/index.js +2 -2
  81. package/dist/orgs/index.d.ts +132 -0
  82. package/dist/orgs/index.js +245 -0
  83. package/dist/orpc/index.d.ts +28 -8
  84. package/dist/orpc/index.js +16 -3
  85. package/dist/otel/index.d.ts +21 -2
  86. package/dist/otel/index.js +50 -9
  87. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  88. package/dist/permdock-sql-DgOfeixi.js +51 -0
  89. package/dist/plugin-DwARMuQp.js +59 -0
  90. package/dist/{plugin-bNhvJwRh.d.ts → plugin-o144f322.d.ts} +17 -5
  91. package/dist/plugins/actor/index.d.ts +5 -2
  92. package/dist/plugins/actor/index.js +22 -8
  93. package/dist/plugins/rules/index.d.ts +2 -2
  94. package/dist/plugins/rules/index.js +15 -10
  95. package/dist/plugins/soft-delete/index.d.ts +3 -3
  96. package/dist/plugins/soft-delete/index.js +14 -5
  97. package/dist/plugins/tenant/index.d.ts +2 -2
  98. package/dist/plugins/tenant/index.js +38 -20
  99. package/dist/plugins/timestamps/index.d.ts +1 -1
  100. package/dist/plugins/timestamps/index.js +4 -3
  101. package/dist/plugins/validation/index.d.ts +2 -2
  102. package/dist/plugins/validation/index.js +26 -6
  103. package/dist/{pool-DFwEQwmN.d.ts → pool-C9voM4Kw.d.ts} +17 -6
  104. package/dist/postgres/index.d.ts +5 -5
  105. package/dist/postgres/index.js +20 -11
  106. package/dist/{postgrest-Bc-0Xw62.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  107. package/dist/{problem-BihjPR0t.js → problem-BW1KhzSG.js} +2 -1
  108. package/dist/{problem-U8UEKccv.d.ts → problem-Blxe8S31.d.ts} +1 -1
  109. package/dist/query/index.d.ts +2 -2
  110. package/dist/query/index.js +3 -3
  111. package/dist/{query-BMMSWbI7.js → query-CCbo44lG.js} +2 -2
  112. package/dist/queue-BYUCjr59.d.ts +252 -0
  113. package/dist/react/index.d.ts +8 -7
  114. package/dist/react/index.js +4 -3
  115. package/dist/react/server.d.ts +14 -3
  116. package/dist/react/server.js +10 -2
  117. package/dist/react/session.d.ts +7 -1
  118. package/dist/react/session.js +9 -1
  119. package/dist/{react-CQHMaESR.js → react-C_s7kiNz.js} +103 -11
  120. package/dist/{read-set-FWK8qQ58.js → read-set-DQFt1N_Y.js} +22 -1
  121. package/dist/realtime/index.d.ts +3 -3
  122. package/dist/realtime/index.js +5 -4
  123. package/dist/{refresh-BwYkyNOQ.d.ts → refresh-CFPX0Qft.d.ts} +1 -1
  124. package/dist/{resource-BJcaIfxi.d.ts → resource-DNRFP7KR.d.ts} +5 -5
  125. package/dist/{resource-My_y-Syh.js → resource-mqfJW4YF.js} +22 -7
  126. package/dist/respond-BR1fJWOB.d.ts +377 -0
  127. package/dist/{result-Bj6yt33D.js → result-BKiliPY7.js} +1 -1
  128. package/dist/server/index.d.ts +15 -12
  129. package/dist/server/index.js +11 -17
  130. package/dist/{server-BA75B0mT.js → server-B1mZ7CgQ.js} +259 -37
  131. package/dist/{shared-DnoCdMDr.js → shared-D1dRUrAg.js} +16 -2
  132. package/dist/signers-Cc6r98SW.d.ts +162 -0
  133. package/dist/{simplify-Cqvfr9dj.js → simplify-_0oUKqCd.js} +4 -115
  134. package/dist/sql/index.d.ts +283 -11
  135. package/dist/sql/index.js +8705 -775
  136. package/dist/{sql-B34HU2DI.js → sql-Coh6n4cC.js} +9 -2
  137. package/dist/ssr/index.d.ts +4 -4
  138. package/dist/ssr/index.js +1 -2
  139. package/dist/{standard-BRyBGTtp.js → standard-D7r6dMUu.js} +6 -3
  140. package/dist/{standard-BsaCYQJc.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  141. package/dist/{stats-C_YxA2r1.js → stats-Bqob3WqT.js} +4 -4
  142. package/dist/storage/index.d.ts +4 -4
  143. package/dist/storage/index.js +155 -26
  144. package/dist/support-CEPwlw2G.d.ts +89 -0
  145. package/dist/support-view-B8SlToQX.d.ts +17 -0
  146. package/dist/support-view-g4YLBeqX.js +18 -0
  147. package/dist/support-wHnK6zp-.js +293 -0
  148. package/dist/{permdock-sql-C4KFzLc9.js → template-TxrGI5jR.js} +2 -31
  149. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  150. package/dist/{temporal-required-CBNHT6ni.js → temporal-required-CAt4Dc9Z.js} +2 -2
  151. package/dist/testing/index.d.ts +132 -10
  152. package/dist/testing/index.js +476 -8
  153. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  154. package/dist/user-change-CHFEUro1.js +49 -0
  155. package/dist/{webhooks-Bh_Jf_-l.js → verify-Ce49ETPz.js} +5 -5
  156. package/dist/{version-Bmf7_FzM.js → version-DOYTYwHP.js} +1 -1
  157. package/dist/{mfa-byfjruR9.js → view-Bn6HNKUe.js} +97 -98
  158. package/dist/{view-B64o81wp.d.ts → view-CbgObAeb.d.ts} +22 -31
  159. package/dist/webhooks/index.d.ts +135 -4
  160. package/dist/webhooks/index.js +588 -2
  161. package/dist/wire-D2dT86vi.js +35 -0
  162. package/package.json +27 -19
  163. package/schemas/config-v1.json +131 -1
  164. package/schemas/doctor-report-v1.json +11 -0
  165. package/skills/better-supabase/SKILL.md +9 -6
  166. package/skills/better-supabase/references/plugins.md +6 -3
  167. package/skills/better-supabase/references/troubleshooting.md +1 -0
  168. package/skills/better-supabase-api/SKILL.md +9 -2
  169. package/skills/better-supabase-api/references/adapters.md +11 -8
  170. package/skills/better-supabase-auth/SKILL.md +18 -7
  171. package/skills/better-supabase-auth/references/permdock.md +78 -11
  172. package/dist/cli/gen-D5cLCULx.js +0 -2
  173. package/dist/cli/sql-D4VbqmXi.js +0 -199
  174. package/dist/events-B21UOhvP.d.ts +0 -84
  175. package/dist/executor-B5IoUxLE.d.ts +0 -195
  176. package/dist/invalidate-CgxmRamI.js +0 -24
  177. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  178. package/dist/plugin-BqR6wKMB.js +0 -23
  179. package/dist/respond-DgIbdrt8.d.ts +0 -194
  180. package/dist/view-BrJI2LKp.js +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-supabase",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue, framework integrations and the better-supabase CLI.",
5
5
  "keywords": [
6
6
  "cli",
@@ -106,6 +106,14 @@
106
106
  "types": "./dist/jobs/index.d.ts",
107
107
  "default": "./dist/jobs/index.js"
108
108
  },
109
+ "./orgs": {
110
+ "types": "./dist/orgs/index.d.ts",
111
+ "default": "./dist/orgs/index.js"
112
+ },
113
+ "./notifications": {
114
+ "types": "./dist/notifications/index.d.ts",
115
+ "default": "./dist/notifications/index.js"
116
+ },
109
117
  "./env": {
110
118
  "types": "./dist/env/index.d.ts",
111
119
  "default": "./dist/env/index.js"
@@ -182,12 +190,12 @@
182
190
  "provenance": true
183
191
  },
184
192
  "dependencies": {
185
- "@standard-schema/spec": "1.1.0",
186
- "@supabase/middleware": "1.0.0",
187
- "@supabase/postgrest-js": "2.117.2",
188
- "@supabase/postgrest-typegen": "0.3.1",
189
- "@supabase/server": "1.9.0",
190
- "@supabase/ssr": "0.12.7"
193
+ "@standard-schema/spec": "^1.1.0",
194
+ "@supabase/middleware": "^1.0.0",
195
+ "@supabase/postgrest-js": "^2.116.0",
196
+ "@supabase/postgrest-typegen": "0.4.0",
197
+ "@supabase/server": "^1.9.0",
198
+ "@supabase/ssr": "^0.12.7"
191
199
  },
192
200
  "devDependencies": {
193
201
  "@arethetypeswrong/cli": "0.18.5",
@@ -196,13 +204,13 @@
196
204
  "@clack/prompts": "1.8.1",
197
205
  "@effect/platform-node": "4.0.0",
198
206
  "@opentelemetry/api": "1.9.1",
199
- "@orpc/server": "2.0.0-beta.40",
207
+ "@orpc/server": "2.0.0-beta.41",
200
208
  "@supabase/config": "0.11.1",
201
209
  "@supabase/supabase-js": "2.117.2",
202
210
  "@t3-oss/env-core": "0.13.11",
203
- "@tanstack/query-core": "5.104.0",
204
- "@tanstack/react-query": "5.104.0",
205
- "@types/node": "26.6.3",
211
+ "@tanstack/query-core": "5.104.1",
212
+ "@tanstack/react-query": "5.104.1",
213
+ "@types/node": "26.6.4",
206
214
  "@types/pg": "8.23.1",
207
215
  "@types/react": "19.3.0",
208
216
  "@types/react-dom": "19.3.0",
@@ -220,7 +228,7 @@
220
228
  "oxfmt": "0.71.0",
221
229
  "oxlint": "1.86.0",
222
230
  "pg": "8.23.1",
223
- "publint": "0.3.24",
231
+ "publint": "0.3.25",
224
232
  "react": "19.3.0",
225
233
  "react-dom": "19.3.0",
226
234
  "smol-toml": "1.9.0",
@@ -233,15 +241,15 @@
233
241
  "zod": "4.6.5"
234
242
  },
235
243
  "peerDependencies": {
236
- "@opentelemetry/api": ">=1.9",
237
- "@orpc/server": ">=2.0.0-0",
244
+ "@opentelemetry/api": ">=1.9 <2",
245
+ "@orpc/server": ">=2.0.0-beta.40 <3",
238
246
  "@supabase/config": ">=0.9.0 <0.12",
239
247
  "@supabase/supabase-js": "^2.116.0",
240
- "@tanstack/query-core": "^5",
241
- "hono": ">=4",
242
- "next": ">=16",
243
- "oxfmt": ">=0.66",
244
- "pg": ">=8",
248
+ "@tanstack/query-core": "^5.62.0",
249
+ "hono": ">=4 <5",
250
+ "next": ">=16 <17",
251
+ "oxfmt": ">=0.66.0 <1",
252
+ "pg": ">=8.15 <9",
245
253
  "react": "^19",
246
254
  "temporal-polyfill": ">=1"
247
255
  },
@@ -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,9 @@
81
89
  "BS406",
82
90
  "BS407",
83
91
  "BS408",
92
+ "BS409",
93
+ "BS410",
94
+ "BS411",
84
95
  "BS501",
85
96
  "BS502"
86
97
  ]
@@ -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.
@@ -61,16 +61,22 @@ A token from the Supabase OAuth server carries `client_id` and `scope`; a
61
61
  token exchanged for an agent carries an RFC 8693 `act` chain. The session
62
62
  names who acts for the user:
63
63
 
64
- | Token | `session.actor` | `session.delegation` |
65
- | ----------------------------------------------- | ------------------------------------ | -------------------- |
66
- | The user's own | not set | not set |
67
- | `client_id: "c1"`, `scope: "openid crm.read"` | `{ id: "c1", kind: "oauth-client" }` | `{ scopes: [...] }` |
68
- | `act: { sub: "agent", act: { sub: "mcp-42" } }` | `{ id: "agent", chain, ... }` | `{ scopes, chain }` |
64
+ | Token | `session.actor` | `session.delegation` |
65
+ | ------------------------------------------------ | ---------------------------------------------- | -------------------- |
66
+ | The user's own | not set | not set |
67
+ | `client_id: "c1"`, `scope: "openid crm.read"` | `{ id: "c1", kind: "oauth-client" }` | `{ scopes: [...] }` |
68
+ | `act: { sub: "agent", act: { sub: "mcp-42" } }` | `{ id: "agent", chain, ... }` | `{ scopes, chain }` |
69
+ | `act: { kind: "support", sub, session_id, ... }` | `{ kind: "support", id, sessionId, readOnly }` | not set |
70
+ | `act: { kind: "impersonation", sub, reason }` | `{ kind: "impersonation", id, reason }` | not set |
69
71
 
70
72
  - Limit what delegated tokens may do with the `scopes` guard option on
71
73
  `bs.route`, `bs.action`, `bs.handler` and `bs.middleware`. A missing
72
- scope answers 403 with an `insufficient_scope` challenge. The user's own
73
- token is never limited by scopes.
74
+ scope answers 403 with an `insufficient_scope` challenge. Only an
75
+ `oauth-client` actor is limited: the user's own token, a support session
76
+ and an impersonated session are not.
77
+ - Switch on `session.actor.kind` with a `never` default: it is
78
+ `oauth-client`, `support` or `impersonation`. An `act` with another `kind`
79
+ resolves to `invalid` with reason `actor`.
74
80
  - In MCP servers, refuse calls in `authorize` by reading
75
81
  `toSession(ctx.auth).delegation?.scopes` (the `better-supabase-api` skill).
76
82
  - RLS still decides the rows: `sub` is the user, so a client sees at most
@@ -106,6 +112,9 @@ membership lasts until the next refresh. When that window matters:
106
112
  - Write policies that read the source of truth, for example
107
113
  `organization_id in (select better_supabase.member_org_ids())`, instead of
108
114
  the claim.
115
+ - For signed-out, banned or deleted users, add the `sessions` SQL module and a
116
+ restrictive policy with `using ((select better_supabase.session_active()))`
117
+ on the tables that guard sensitive data.
109
118
  - After a change the user made themselves (joining an organization), call
110
119
  `supabase.auth.refreshSession()` in the browser.
111
120
  - In Next.js, drop the user's cached views with
@@ -136,6 +145,8 @@ claims, tenant or entitlements.
136
145
  - Don't downgrade an `invalid` caller to `anon`.
137
146
  - Don't build 401 or 403 JSON by hand; return the `DbError` or use `allow`
138
147
  and `scopes`.
148
+ - Don't use `allow: ['anon']` for users from `signInAnonymously()`: `'anon'`
149
+ means no session, and only `'anonymous'` admits anonymous sign-ins.
139
150
 
140
151
  ## Docs
141
152
 
@@ -26,30 +26,97 @@ 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>`.
99
+ - For the SQL kit, set `kits.access.model: 'permdock'` and
100
+ `kits.access.functions.canAssign: 'permdock.permdock_can_assign({role}, {tenant}::text)'`
101
+ (use the manifest's `rls.schema`), then run `permdock supabase inspect --out`
102
+ before `sql add`. The kit reads the schema, the root scope and its id type
103
+ from `permdock.manifest.json`, and `sql add` stops instead of guessing when
104
+ the manifest can't give them. Every permission key the kit modules check
105
+ (`kitPermissionKeys` from `better-supabase/sql`) must be
106
+ `rowConditions: false` in `permissions.catalog.json`; map others with
107
+ `kits.<module>.permissions`. Doctor reports BS411.
108
+ - Under that model `can_user()` and `member_can()` answer for the caller only
109
+ and raise SQLSTATE `0A000` for another user. Invitations skip the inviter
110
+ re-check at accept time and notifications don't filter recipients by read
111
+ permission; check both in the app when they matter.
112
+ - The `act` claim says who acts: `kind: "support"` (support sessions,
113
+ `session.actor.kind === 'support'` with `sessionId` and `readOnly`),
114
+ `kind: "impersonation"` (`actingAs`), or no `kind` for an OAuth client or
115
+ agent chain (`oauth-client`, limited by `scopes`). Another `kind` makes the
116
+ session invalid. `session.impersonator` is set for the first two only.
117
+ `supabaseClaimFixtures` in `better-supabase/testing` has one token of each.
118
+ - Only `allow: ['anonymous']` admits `signInAnonymously()` users; `'anon'`
119
+ means no session. Pair the default `allow` with PermDock's
120
+ `rls: { anonymousSignIns: 'deny' }` in `permdock.config.ts`.
54
121
 
55
122
  Docs: https://bettersupabase.com/docs/auth/permdock.md
@@ -1,2 +0,0 @@
1
- import { n as genCommand } from "./gen-Ccn_p90L.js";
2
- export { genCommand };