better-supabase 0.1.0 → 0.3.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 (149) hide show
  1. package/README.md +34 -31
  2. package/dist/base64-CQeB1eSf.js +42 -0
  3. package/dist/claims-CoTobNC-.js +25 -0
  4. package/dist/client/index.d.ts +1 -1
  5. package/dist/client/index.js +4 -14
  6. package/dist/{compiler-DL18ukPe.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
  7. package/dist/config/index.d.ts +4 -3
  8. package/dist/config/index.js +5 -3
  9. package/dist/config-Be4GwsqI.js +129 -0
  10. package/dist/{index-BD6wKthp.d.ts → config-C1CYcm6o.d.ts} +75 -54
  11. package/dist/{config-D-aR2ZoN.js → config-DOsGYSiT.js} +14 -136
  12. package/dist/{define-QGkq04oe.js → define-DF341GDj.js} +133 -110
  13. package/dist/{define-CTfWw-SS.d.ts → define-DUbydmJa.d.ts} +32 -14
  14. package/dist/edge/index.d.ts +4 -4
  15. package/dist/edge/index.js +4 -4
  16. package/dist/entitlements-CTEBm7IB.js +20 -0
  17. package/dist/entitlements-DvJXoI02.d.ts +42 -0
  18. package/dist/env/index.js +203 -1
  19. package/dist/events/index.d.ts +1 -1
  20. package/dist/events/index.js +139 -1
  21. package/dist/{events-2OJsVG_d.d.ts → events-Do8jXh4D.d.ts} +3 -3
  22. package/dist/{executor-Bje4suQF.d.ts → executor-B26lgbHD.d.ts} +2 -2
  23. package/dist/{executor-Ceq60hPM.js → executor-C57jx2pn.js} +7 -2
  24. package/dist/{executor-Bazhgsgq.d.ts → executor-DynmIWhq.d.ts} +2 -2
  25. package/dist/guards-BnzDvFrz.js +10 -0
  26. package/dist/hono/index.d.ts +5 -5
  27. package/dist/hono/index.js +4 -4
  28. package/dist/{index-Hf8PP9sP.d.ts → index-BNsABq7P.d.ts} +17 -13
  29. package/dist/{index-JXHBuS53.d.ts → index-CADlNHK-.d.ts} +27 -14
  30. package/dist/{index-Nxo3TJcm.d.ts → index-CTTMS6_1.d.ts} +27 -8
  31. package/dist/index-Cf-D2jk1.d.ts +50 -0
  32. package/dist/{index-Dup-nSe9.d.ts → index-CriFFJ_p.d.ts} +4 -4
  33. package/dist/{index-DQVvHzjW.d.ts → index-GV77NpRP.d.ts} +5 -4
  34. package/dist/{index-Sw43J8HG.d.ts → index-UJ-LUxtI.d.ts} +6 -6
  35. package/dist/{index-cEeweTeM.d.ts → index-kpgopLNv.d.ts} +4 -3
  36. package/dist/{index-DRid0qIv.d.ts → index-qriRM9cZ.d.ts} +7 -2
  37. package/dist/index.d.ts +13 -13
  38. package/dist/index.js +18 -8
  39. package/dist/jobs/index.d.ts +25 -24
  40. package/dist/jobs/index.js +19 -12
  41. package/dist/{json-schema-BbwO2mZg.js → json-schema--aOXAGfU.js} +9 -3
  42. package/dist/lint/index.js +1 -1
  43. package/dist/list/index.d.ts +2 -2
  44. package/dist/list/index.js +128 -93
  45. package/dist/{live-6b9i72Ux.d.ts → live-QE3UPbGm.d.ts} +27 -6
  46. package/dist/logger-glM28NVN.js +21 -0
  47. package/dist/mcp/index.d.ts +59 -15
  48. package/dist/mcp/index.js +229 -67
  49. package/dist/{mfa-CpogP66z.js → mfa-BxoR1RkN.js} +97 -22
  50. package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
  51. package/dist/next/index.d.ts +51 -38
  52. package/dist/next/index.js +31 -25
  53. package/dist/openapi/index.d.ts +1 -1
  54. package/dist/openapi/index.js +58 -35
  55. package/dist/orpc/index.d.ts +6 -6
  56. package/dist/orpc/index.js +3 -3
  57. package/dist/otel/index.d.ts +3 -3
  58. package/dist/otel/index.js +5 -3
  59. package/dist/{template-BeOGnynM.js → permdock-sql-Bta_G0v-.js} +32 -3
  60. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  61. package/dist/{plugin-HvGqCurB.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
  62. package/dist/plugins/actor/index.d.ts +1 -1
  63. package/dist/plugins/rules/index.d.ts +4 -3
  64. package/dist/plugins/rules/index.js +8 -15
  65. package/dist/plugins/soft-delete/index.d.ts +3 -3
  66. package/dist/plugins/soft-delete/index.js +2 -2
  67. package/dist/plugins/tenant/index.d.ts +10 -4
  68. package/dist/plugins/tenant/index.js +15 -13
  69. package/dist/plugins/timestamps/index.d.ts +1 -1
  70. package/dist/plugins/timestamps/index.js +1 -1
  71. package/dist/plugins/validation/index.d.ts +2 -2
  72. package/dist/pool-B9qsLyFR.d.ts +71 -0
  73. package/dist/postgres/index.d.ts +10 -5
  74. package/dist/postgres/index.js +36 -16
  75. package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
  76. package/dist/{problem-DPu6Fh-t.js → problem-CGt1ADix.js} +14 -8
  77. package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
  78. package/dist/query/index.d.ts +2 -2
  79. package/dist/query/index.js +1 -1
  80. package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
  81. package/dist/react/index.d.ts +7 -7
  82. package/dist/react/index.js +3 -3
  83. package/dist/react/server.d.ts +5 -5
  84. package/dist/react/server.js +1 -1
  85. package/dist/react/session.d.ts +21 -2
  86. package/dist/react/session.js +19 -1
  87. package/dist/{react-CrOvrnIP.js → react-DNBYNazu.js} +28 -23
  88. package/dist/{read-set-DhX2d04c.js → read-set-D0qBcboM.js} +3 -3
  89. package/dist/realtime/index.d.ts +4 -2
  90. package/dist/realtime/index.js +30 -14
  91. package/dist/{resolve-DW8h9NPm.d.ts → resolve-BRciX2jK.d.ts} +25 -9
  92. package/dist/{resource-SkPTRJDD.d.ts → resource-CwVgiI5q.d.ts} +16 -7
  93. package/dist/{resource-CYiu6Ygi.js → resource-D30rp0GN.js} +38 -16
  94. package/dist/{respond-4-dhoV7x.js → respond-C44EZAJo.js} +38 -19
  95. package/dist/{respond-PoTpy5DT.d.ts → respond-DAkPIdTH.d.ts} +23 -17
  96. package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
  97. package/dist/server/index.d.ts +35 -10
  98. package/dist/server/index.js +38 -9
  99. package/dist/{simplify-oHiDKRVQ.js → simplify-LakqK1vH.js} +22 -28
  100. package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
  101. package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
  102. package/dist/sql/index.d.ts +139 -0
  103. package/dist/sql/index.js +1983 -0
  104. package/dist/{sql-M8l3fnS0.js → sql-BIgk12ew.js} +72 -16
  105. package/dist/ssr/index.d.ts +5 -5
  106. package/dist/ssr/index.js +3 -3
  107. package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
  108. package/dist/{stats-3iy8u3_P.js → stats-D78UQ8Yu.js} +8 -7
  109. package/dist/storage/index.d.ts +4 -2
  110. package/dist/storage/index.js +530 -1
  111. package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
  112. package/dist/temporal-BwshQk0W.js +27 -0
  113. package/dist/temporal-required-CMxeSVPo.js +19 -0
  114. package/dist/testing/index.d.ts +49 -16
  115. package/dist/testing/index.js +233 -66
  116. package/dist/{types-C2fslP1z.d.ts → types-DONv9SHN.d.ts} +68 -12
  117. package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
  118. package/dist/view-3Po_PVlr.d.ts +79 -0
  119. package/dist/{view-B6F9QbG-.js → view-wZ-9qIUz.js} +12 -2
  120. package/dist/webhooks/index.d.ts +19 -17
  121. package/dist/webhooks/index.js +1 -1
  122. package/dist/{webhooks-DbMTJJX2.js → webhooks-CAIFQSTM.js} +12 -10
  123. package/package.json +30 -30
  124. package/schemas/config-v1.json +80 -5
  125. package/schemas/doctor-report-v1.json +18 -1
  126. package/schemas/snapshot-v2.json +39 -0
  127. package/skills/better-supabase/SKILL.md +23 -11
  128. package/skills/better-supabase/references/plugins.md +28 -16
  129. package/skills/better-supabase/references/troubleshooting.md +15 -15
  130. package/skills/better-supabase-api/SKILL.md +9 -9
  131. package/skills/better-supabase-api/references/adapters.md +30 -25
  132. package/skills/better-supabase-testing/SKILL.md +15 -11
  133. package/bin/better-supabase.js +0 -3
  134. package/dist/casing-da0uRTqt.js +0 -14
  135. package/dist/cli/bin.d.ts +0 -1
  136. package/dist/cli/bin.js +0 -15
  137. package/dist/cli/index.d.ts +0 -226
  138. package/dist/cli/index.js +0 -3
  139. package/dist/cli-CoByWYja.js +0 -6472
  140. package/dist/entitlements-BBg61DQZ.d.ts +0 -20
  141. package/dist/entitlements-BC_khwRy.js +0 -17
  142. package/dist/env-BEToab0I.js +0 -204
  143. package/dist/events-BuzTgn2D.js +0 -135
  144. package/dist/pool-BFeqzwQS.d.ts +0 -37
  145. package/dist/seed-CzecODcJ.js +0 -101
  146. package/dist/session-C3osL21k.d.ts +0 -21
  147. package/dist/session-CD-3orO_.js +0 -19
  148. package/dist/storage-BWXVUXGk.js +0 -461
  149. package/dist/view-B0ltUmTi.d.ts +0 -43
@@ -67,7 +67,7 @@
67
67
  "type": "object",
68
68
  "additionalProperties": false,
69
69
  "properties": {
70
- "timestamptz": { "enum": ["string", "date"], "default": "string" },
70
+ "timestamptz": { "enum": ["string", "instant"], "default": "string" },
71
71
  "int8": { "enum": ["number", "string", "bigint"], "default": "number" },
72
72
  "numeric": { "enum": ["number", "string"], "default": "number" }
73
73
  }
@@ -148,8 +148,7 @@
148
148
  "type": "object",
149
149
  "additionalProperties": false,
150
150
  "properties": {
151
- "column": { "type": "string" },
152
- "claim": { "type": "string" }
151
+ "column": { "type": "string" }
153
152
  }
154
153
  }
155
154
  ]
@@ -169,6 +168,25 @@
169
168
  }
170
169
  }
171
170
  },
171
+ "claims": {
172
+ "description": "Claim names shared by the SQL kit, codegen and runtime defaults (PermDock's claim contract).",
173
+ "type": "object",
174
+ "additionalProperties": false,
175
+ "properties": {
176
+ "tenant": {
177
+ "type": "string",
178
+ "description": "Top-level claim holding the active tenant id. Defaults to `tenant_id`."
179
+ },
180
+ "scope": {
181
+ "type": "string",
182
+ "description": "`scope` of the tenant entries in the `memberships` claim. Defaults to `tenant`."
183
+ },
184
+ "features": {
185
+ "type": "string",
186
+ "description": "Claim holding plan features per tenant. Defaults to `features`."
187
+ }
188
+ }
189
+ },
172
190
  "buckets": {
173
191
  "type": "object",
174
192
  "additionalProperties": {
@@ -182,7 +200,33 @@
182
200
  "type": "string",
183
201
  "description": "Path template, e.g. `{orgId}/{customerId}/logo.webp`."
184
202
  },
185
- "policy": { "enum": ["tenant", "owner", "public", "none"] },
203
+ "policy": {
204
+ "oneOf": [
205
+ { "enum": ["tenant", "owner", "public", "none"] },
206
+ {
207
+ "type": "object",
208
+ "description": "Policies that call PermDock's permitted_<scope>_ids or permdock_has helpers.",
209
+ "required": ["permdock", "scope"],
210
+ "additionalProperties": false,
211
+ "properties": {
212
+ "permdock": {
213
+ "type": "object",
214
+ "required": ["read", "write"],
215
+ "additionalProperties": false,
216
+ "properties": {
217
+ "read": { "type": "string" },
218
+ "list": { "type": "string" },
219
+ "write": { "type": "string" },
220
+ "delete": { "type": "string" }
221
+ }
222
+ },
223
+ "scope": { "type": "string" },
224
+ "segment": { "type": "integer", "minimum": 1 },
225
+ "schema": { "type": "string" }
226
+ }
227
+ }
228
+ ]
229
+ },
186
230
  "fileSizeLimit": { "type": "string" },
187
231
  "allowedMimeTypes": { "type": "array", "items": { "type": "string" } }
188
232
  }
@@ -239,6 +283,36 @@
239
283
  "description": "The tenant id column of that table.",
240
284
  "type": "string",
241
285
  "default": "id"
286
+ },
287
+ "permdock": {
288
+ "description": "With a PermDock manifest, read PermDock's memberships for this scope instead of better_supabase.memberships; false keeps the kit's table.",
289
+ "oneOf": [
290
+ { "const": false },
291
+ {
292
+ "type": "object",
293
+ "additionalProperties": false,
294
+ "properties": {
295
+ "scope": { "type": "string", "default": "organization" }
296
+ }
297
+ }
298
+ ]
299
+ }
300
+ }
301
+ },
302
+ "permdock": {
303
+ "description": "Where PermDock's JSON outputs are.",
304
+ "type": "object",
305
+ "additionalProperties": false,
306
+ "properties": {
307
+ "manifest": {
308
+ "description": "What `permdock supabase inspect --out` writes.",
309
+ "type": "string",
310
+ "default": "permdock.manifest.json"
311
+ },
312
+ "catalog": {
313
+ "description": "What `permdock catalog` writes.",
314
+ "type": "string",
315
+ "default": "permissions.catalog.json"
242
316
  }
243
317
  }
244
318
  },
@@ -314,7 +388,8 @@
314
388
  },
315
389
  "strict": { "type": "boolean" },
316
390
  "sources": { "type": "array", "items": { "type": "string" } },
317
- "policyHelperLimit": { "type": "integer", "minimum": 1 }
391
+ "policyHelperLimit": { "type": "integer", "minimum": 1 },
392
+ "claimsLimit": { "type": "integer", "minimum": 1 }
318
393
  }
319
394
  }
320
395
  },
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://unpkg.com/better-supabase/schemas/doctor-report-v1.json",
4
4
  "title": "better-supabase doctor report",
5
- "description": "Output of `better-supabase doctor --format json`.",
5
+ "description": "Output of `better-supabase doctor --json`.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["$schema", "version", "tool", "summary", "findings"],
@@ -42,6 +42,12 @@
42
42
  "BS106",
43
43
  "BS107",
44
44
  "BS108",
45
+ "BS109",
46
+ "BS110",
47
+ "BS111",
48
+ "BS112",
49
+ "BS113",
50
+ "BS114",
45
51
  "BS200",
46
52
  "BS204",
47
53
  "BS205",
@@ -52,6 +58,15 @@
52
58
  "BS210",
53
59
  "BS211",
54
60
  "BS212",
61
+ "BS213",
62
+ "BS214",
63
+ "BS215",
64
+ "BS216",
65
+ "BS217",
66
+ "BS218",
67
+ "BS219",
68
+ "BS220",
69
+ "BS221",
55
70
  "BS301",
56
71
  "BS302",
57
72
  "BS303",
@@ -64,6 +79,8 @@
64
79
  "BS404",
65
80
  "BS405",
66
81
  "BS406",
82
+ "BS407",
83
+ "BS408",
67
84
  "BS501",
68
85
  "BS502"
69
86
  ]
@@ -690,6 +690,12 @@
690
690
  "additionalProperties": {
691
691
  "type": "string"
692
692
  }
693
+ },
694
+ "execute": {
695
+ "type": "array",
696
+ "items": {
697
+ "type": "string"
698
+ }
693
699
  }
694
700
  }
695
701
  }
@@ -766,6 +772,10 @@
766
772
  "items": {
767
773
  "type": "string"
768
774
  }
775
+ },
776
+ "source": {
777
+ "type": "string",
778
+ "description": "The function body (`pg_proc.prosrc`)."
769
779
  }
770
780
  }
771
781
  }
@@ -897,6 +907,12 @@
897
907
  },
898
908
  "partial": {
899
909
  "type": "boolean"
910
+ },
911
+ "method": {
912
+ "type": "string"
913
+ },
914
+ "predicate": {
915
+ "type": ["string", "null"]
900
916
  }
901
917
  }
902
918
  }
@@ -992,6 +1008,29 @@
992
1008
  }
993
1009
  }
994
1010
  },
1011
+ "columnGrants": {
1012
+ "description": "Column-level insert and update grants to anon and authenticated. Absent in snapshots taken before they were introspected.",
1013
+ "type": "array",
1014
+ "items": {
1015
+ "type": "object",
1016
+ "additionalProperties": false,
1017
+ "required": ["column", "role", "privileges"],
1018
+ "properties": {
1019
+ "column": {
1020
+ "type": "string"
1021
+ },
1022
+ "role": {
1023
+ "type": "string"
1024
+ },
1025
+ "privileges": {
1026
+ "type": "array",
1027
+ "items": {
1028
+ "type": "string"
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ },
995
1034
  "large": {
996
1035
  "description": "Postgres estimates at least 10,000 rows; the unbounded-read lint rule reads it.",
997
1036
  "const": true
@@ -11,13 +11,22 @@ The data layer is `sb = defineSupabase(schema)` in `src/lib/supabase.ts`.
11
11
 
12
12
  ## Workflow: change the schema
13
13
 
14
- 1. Write the migration (`supabase/schemas/*.sql` plus `supabase db diff`, or
15
- `supabase migration new`).
16
- 2. Apply it with `supabase db reset` (or `supabase migration up`).
17
- 3. Run `pnpm better-supabase gen`, then fix the type errors it surfaces.
18
- 4. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
19
- missing policies, unindexed foreign keys, drift).
20
- 5. Commit the generated files. CI runs `better-supabase gen --check`.
14
+ 1. Write the migration: edit `supabase/schemas/*.sql`, then run
15
+ `supabase db schema declarative sync -f <name>` (pg-delta;
16
+ `supabase db diff -f <name>` when `config.toml` has no
17
+ `[experimental.pgdelta] enabled = true`), or `supabase migration new` for
18
+ data and role settings.
19
+ 2. For a new table in an exposed schema, in the same file: `enable row level
20
+ security`, a policy per role and command the app uses, and an index on
21
+ every column a policy filters by. List the table in `expose` in
22
+ `better-supabase.config.ts` and run `pnpm better-supabase sql add grants`:
23
+ new tables get no Data API grants on their own.
24
+ 3. Apply it with `supabase db reset` (or `supabase migration up`).
25
+ 4. Run `pnpm better-supabase gen`, then fix the type errors it surfaces.
26
+ The command comes from the `@better-supabase/cli` dev dependency.
27
+ 5. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
28
+ missing policies, missing grants, unindexed foreign keys, drift).
29
+ 6. Commit the generated files. CI runs `better-supabase gen --check`.
21
30
 
22
31
  Done when `gen --check` and `doctor` exit 0 and the project typechecks.
23
32
 
@@ -48,10 +57,10 @@ stay hidden.
48
57
 
49
58
  ```ts
50
59
  const result = await db.customers.findMany({
51
- select: ['id', 'name'],
52
- where: { status: 'active', notes: { some: { kind: 'call' } } },
53
- include: { organization: { select: ['name'] } },
54
- orderBy: { name: 'asc' },
60
+ select: ["id", "name"],
61
+ where: { status: "active", notes: { some: { kind: "call" } } },
62
+ include: { organization: { select: ["name"] } },
63
+ orderBy: { name: "asc" },
55
64
  limit: 20,
56
65
  });
57
66
  if (!result.ok) return result; // DbError: kind, message, status, code
@@ -82,3 +91,6 @@ See [references/troubleshooting.md](references/troubleshooting.md) for each
82
91
  https://bettersupabase.com/docs. Every page is also served as Markdown at
83
92
  `https://bettersupabase.com/docs/<path>.md`, and
84
93
  https://bettersupabase.com/llms.txt lists them all.
94
+
95
+ For Supabase itself (Auth, Storage, RLS, Postgres performance), install
96
+ Supabase's skills next to these: `npx skills add supabase/agent-skills`.
@@ -1,15 +1,15 @@
1
1
  # Plugins
2
2
 
3
3
  ```ts title="src/lib/supabase.ts"
4
- import { defineSupabase } from 'better-supabase';
5
- import { actor } from 'better-supabase/plugins/actor';
6
- import { softDelete } from 'better-supabase/plugins/soft-delete';
7
- import { tenant } from 'better-supabase/plugins/tenant';
8
- import { timestamps } from 'better-supabase/plugins/timestamps';
9
- import { validation } from 'better-supabase/plugins/validation';
4
+ import { defineSupabase } from "better-supabase";
5
+ import { actor } from "better-supabase/plugins/actor";
6
+ import { softDelete } from "better-supabase/plugins/soft-delete";
7
+ import { tenant } from "better-supabase/plugins/tenant";
8
+ import { timestamps } from "better-supabase/plugins/timestamps";
9
+ import { validation } from "better-supabase/plugins/validation";
10
10
 
11
- import { schema } from './supabase/generated.ts';
12
- import { validators } from './supabase/generated.zod.ts';
11
+ import { schema } from "./supabase/generated.ts";
12
+ import { validators } from "./supabase/generated.zod.ts";
13
13
 
14
14
  export const sb = defineSupabase(schema)
15
15
  .use(timestamps())
@@ -34,14 +34,14 @@ plugins: {
34
34
  A table without the columns is left alone, and the types follow:
35
35
  `restore()` and `withDeleted` only exist on soft-delete tables.
36
36
 
37
- | Plugin | What it does | Don't |
38
- | --- | --- | --- |
39
- | `timestamps()` | Sets `createdAt` and `updatedAt` | set them in `create` or `update` |
40
- | `softDelete()` | Hides deleted rows, turns `delete` into an update, adds `restore` | filter `deletedAt: null` by hand |
41
- | `tenant()` | Scopes queries to the request's tenant and fills it on insert | pass `organizationId` from the client |
42
- | `actor()` | Sets `createdBy` and `updatedBy` | pass the user id yourself |
43
- | `validation()` | Validates writes with any Standard Schema | validate the same input twice |
44
- | `rules()` | Flags unbounded reads, missing tenants, sensitive columns and admin keys in the browser | disable a rule without a reason |
37
+ | Plugin | What it does | Don't |
38
+ | -------------- | --------------------------------------------------------------------------------------- | ------------------------------------- |
39
+ | `timestamps()` | Sets `createdAt` and `updatedAt` | set them in `create` or `update` |
40
+ | `softDelete()` | Hides deleted rows, turns `delete` into an update, adds `restore` | filter `deletedAt: null` by hand |
41
+ | `tenant()` | Scopes queries to the request's tenant and fills it on insert | pass `organizationId` from the client |
42
+ | `actor()` | Sets `createdBy` and `updatedBy` | pass the user id yourself |
43
+ | `validation()` | Validates writes with any Standard Schema | validate the same input twice |
44
+ | `rules()` | Flags unbounded reads, missing tenants, sensitive columns and admin keys in the browser | disable a rule without a reason |
45
45
 
46
46
  ## Order
47
47
 
@@ -53,4 +53,16 @@ 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
59
+ has a `permdock.config.ts`, PermDock's hook writes that claim and the
60
+ memberships; don't run `sql add tenant` there or call `membership_claims()`
61
+ from a hook. `sql add entitlements` is fine: `features` is not a PermDock
62
+ claim. Add `claims: { features: 'better_supabase.feature_claims' }` to
63
+ `supabase.hook` in `permdock.config.ts`, run `permdock supabase hook generate`,
64
+ and `hasEntitlement(session, ...)` works. Run `permdock supabase inspect --out`
65
+ first: with `permdock.manifest.json` present, the entitlements module reads
66
+ PermDock's `member_<scope>_ids` helpers and doesn't add `tenant`.
67
+
56
68
  Docs: https://bettersupabase.com/docs/plugins.md
@@ -7,21 +7,21 @@ Start with `pnpm better-supabase doctor`. It checks the database, RLS,
7
7
 
8
8
  Branch on `result.error.kind`, not on the message.
9
9
 
10
- | Kind | Usual cause | Fix |
11
- | --- | --- | --- |
12
- | `not_found` | The row doesn't exist, or RLS hides it from this user | Check the policy with an `asUser` test before assuming the row is gone |
13
- | `unauthorized` | No valid session or token | Get `db` from the adapter for this request |
14
- | `forbidden` | RLS or a grant rejected the write | Fix the policy or the grant; don't switch to `server.admin()` |
15
- | `conflict` | Unique violation | Use `upsert`, or return the error to the caller |
16
- | `foreign_key` | Referenced row missing, or still referenced on delete | Create the parent first, or delete children |
17
- | `not_null`, `check`, `exclusion` | A constraint rejected the row | Validate input earlier with the `validation()` plugin |
18
- | `invalid_input` | Postgres couldn't parse a value (bad uuid, enum) | Validate before the call |
19
- | `validation` | A Standard Schema rejected the input; see `issues` | Show the issues to the user |
20
- | `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
- | `multiple_rows` | `findUnique` or a single-row write matched more than one row | Filter by a unique key |
22
- | `stale` | `update(..., { expect })` found a newer row | Reload the row and retry |
23
- | `serialization`, `timeout`, `network`, `rate_limited` | Transient | Retry with backoff, or surface the error |
24
- | `raised` | A database function raised an exception | Read `message` and `code` from the function |
10
+ | Kind | Usual cause | Fix |
11
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
12
+ | `not_found` | The row doesn't exist, or RLS hides it from this user | Check the policy with an `asUser` test before assuming the row is gone |
13
+ | `unauthorized` | No valid session or token | Get `db` from the adapter for this request |
14
+ | `forbidden` | RLS or a grant rejected the write | Fix the policy or the grant; don't switch to `server.admin()` |
15
+ | `conflict` | Unique violation | Use `upsert`, or return the error to the caller |
16
+ | `foreign_key` | Referenced row missing, or still referenced on delete | Create the parent first, or delete children |
17
+ | `not_null`, `check`, `exclusion` | A constraint rejected the row | Validate input earlier with the `validation()` plugin |
18
+ | `invalid_input` | Postgres couldn't parse a value (bad uuid, enum) | Validate before the call |
19
+ | `validation` | A Standard Schema rejected the input; see `issues` | Show the issues to the user |
20
+ | `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
+ | `multiple_rows` | `findUnique` or a single-row write matched more than one row | Filter by a unique key |
22
+ | `stale` | `update(..., { expect })` found a newer row | Reload the row and retry |
23
+ | `serialization`, `timeout`, `network`, `rate_limited` | Transient | Retry with backoff, or surface the error |
24
+ | `raised` | A database function raised an exception | Read `message` and `code` from the function |
25
25
 
26
26
  ## Other symptoms
27
27
 
@@ -32,13 +32,13 @@ and `['service']` for machine callers. Rejected callers get a 401 with a
32
32
 
33
33
  ## Adapters
34
34
 
35
- | Where | Setup | Handler |
36
- | --- | --- | --- |
37
- | Next.js | `createNext(sb)` in `lib/supabase.server.ts` | `next.route((req, { db }) => ...)`, `next.action({ input: schema }, (input, { db }) => ...)` |
38
- | Hono | `createHono(sb)`, `.use('/api/*', bs.middleware())` | `c.var.db`; `bs.resource('customers', {...})` for REST |
39
- | oRPC | `createOrpc(sb)`, `base.use(bs.middleware())` | `bs.unwrap(context.db.customers.findMany(...))` |
40
- | Edge Functions | `createEdge(sb, { cors: true })` | `Deno.serve(bs.handler((req, { db }) => ...))` |
41
- | MCP | `createMcp(sb, { name, version, resources })` | `.tool({ name, input, run: (args, { db }) => ... })` |
35
+ | Where | Setup | Handler |
36
+ | -------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
37
+ | Next.js | `createNext(sb)` in `lib/supabase.server.ts` | `next.route((req, { db }) => ...)`, `next.action({ input: schema }, (input, { db }) => ...)` |
38
+ | Hono | `createHono(sb)`, `.use('/api/*', bs.middleware())` | `c.var.db`; `bs.resource('customers', {...})` for REST |
39
+ | oRPC | `createOrpc(sb)`, `base.use(bs.middleware())` | `bs.unwrap(context.db.customers.findMany(...))` |
40
+ | Edge Functions | `createEdge(sb, { cors: true })` | `Deno.serve(bs.handler((req, { db }) => ...))` |
41
+ | MCP | `createMcp(sb, { name, version, resources })` | `.tool({ name, input, run: (args, { db }) => ... })` |
42
42
 
43
43
  Don't build error JSON by hand; errors become Problem Details.
44
44
 
@@ -60,8 +60,8 @@ and a replayed webhook or POST doesn't run twice.
60
60
 
61
61
  ## Testing
62
62
 
63
- Use `localAuth(secret)` as a resolver and `signTestJwt` / `asUser` from
64
- `better-supabase/testing`. See the `better-supabase-testing` skill.
63
+ Sign tokens with `signLocalJwt` or `asUser` from `better-supabase/testing`;
64
+ the adapter verifies them against the local JWKS. See the `better-supabase-testing` skill.
65
65
 
66
66
  Docs: https://bettersupabase.com/docs/frameworks/hono.md (and `next`,
67
67
  `orpc`, `edge`, `mcp` under `/docs/frameworks/`).
@@ -3,11 +3,11 @@
3
3
  Each adapter wraps the same `sb` from `src/lib/supabase.ts`:
4
4
 
5
5
  ```ts title="src/lib/supabase.ts"
6
- import { defineSupabase } from 'better-supabase';
6
+ import { defineSupabase } from "better-supabase";
7
7
 
8
- import { schema } from './supabase/generated';
8
+ import { schema } from "./supabase/generated";
9
9
 
10
- export type { Functions, Models } from './supabase/generated';
10
+ export type { Functions, Models } from "./supabase/generated";
11
11
 
12
12
  export const sb = defineSupabase(schema);
13
13
  ```
@@ -15,17 +15,17 @@ export const sb = defineSupabase(schema);
15
15
  ## Next.js
16
16
 
17
17
  ```ts title="src/lib/supabase.server.ts"
18
- import { createNext } from 'better-supabase/next';
18
+ import { createNext } from "better-supabase/next";
19
19
 
20
- import { sb } from './supabase';
20
+ import { sb } from "./supabase";
21
21
 
22
22
  export const next = createNext(sb);
23
23
  ```
24
24
 
25
25
  ```ts title="src/proxy.ts"
26
- import type { NextRequest } from 'next/server';
26
+ import type { NextRequest } from "next/server";
27
27
 
28
- import { next } from './lib/supabase.server';
28
+ import { next } from "./lib/supabase.server";
29
29
 
30
30
  export const proxy = (request: NextRequest) => next.proxy(request);
31
31
  ```
@@ -35,24 +35,29 @@ route handlers and actions read the verified token:
35
35
 
36
36
  ```ts
37
37
  const { db } = await next.server();
38
- export const GET = next.route((request, { db }) => db.customers.findMany({ limit: 20 }));
38
+ export const GET = next.route((request, { db }) =>
39
+ db.customers.findMany({ limit: 20 }),
40
+ );
39
41
  ```
40
42
 
41
43
  ## Hono
42
44
 
43
45
  ```ts title="src/server.ts"
44
- import { type BetterEnv, createHono } from 'better-supabase/hono';
45
- import { Hono } from 'hono';
46
+ import { type BetterEnv, createHono } from "better-supabase/hono";
47
+ import { Hono } from "hono";
46
48
 
47
- import { type Functions, type Models, sb } from './lib/supabase';
49
+ import { type Functions, type Models, sb } from "./lib/supabase";
48
50
 
49
51
  const bs = createHono(sb);
50
52
 
51
53
  const app = new Hono<BetterEnv<Models, Functions, unknown>>()
52
54
  .onError(bs.onError)
53
- .use('/api/*', bs.middleware())
54
- .get('/api/me', (c) => c.json({ kind: c.var.auth.kind }))
55
- .route('/api/customers', bs.resource('customers', { select: ['id', 'name'] }));
55
+ .use("/api/*", bs.middleware())
56
+ .get("/api/me", (c) => c.json({ kind: c.var.auth.kind }))
57
+ .route(
58
+ "/api/customers",
59
+ bs.resource("customers", { select: ["id", "name"] }),
60
+ );
56
61
 
57
62
  export default app;
58
63
  ```
@@ -60,10 +65,10 @@ export default app;
60
65
  ## oRPC
61
66
 
62
67
  ```ts title="src/router.ts"
63
- import { os } from '@orpc/server';
64
- import { createOrpc, type OrpcRequestContext } from 'better-supabase/orpc';
68
+ import { os } from "@orpc/server";
69
+ import { createOrpc, type OrpcRequestContext } from "better-supabase/orpc";
65
70
 
66
- import { sb } from './lib/supabase';
71
+ import { sb } from "./lib/supabase";
67
72
 
68
73
  export const bs = createOrpc(sb);
69
74
  const authed = os.$context<OrpcRequestContext>().use(bs.middleware());
@@ -78,28 +83,28 @@ export const router = {
78
83
  ## Edge Functions
79
84
 
80
85
  ```ts title="supabase/functions/api/index.ts"
81
- import { createEdge } from 'better-supabase/edge';
86
+ import { createEdge } from "better-supabase/edge";
82
87
 
83
- import { sb } from '../_shared/supabase.ts';
88
+ import { sb } from "../_shared/supabase.ts";
84
89
 
85
90
  const bs = createEdge(sb, { cors: true });
86
91
 
87
92
  Deno.serve(
88
- bs.resources({ customers: { select: ['id', 'name'] } }, { basePath: '/api' }),
93
+ bs.resources({ customers: { select: ["id", "name"] } }, { basePath: "/api" }),
89
94
  );
90
95
  ```
91
96
 
92
97
  ## MCP
93
98
 
94
99
  ```ts title="supabase/functions/mcp/index.ts"
95
- import { createMcp } from 'better-supabase/mcp';
100
+ import { createMcp } from "better-supabase/mcp";
96
101
 
97
- import { sb } from '../_shared/supabase.ts';
102
+ import { sb } from "../_shared/supabase.ts";
98
103
 
99
104
  const mcp = createMcp(sb, {
100
- name: 'crm',
101
- version: '0.1.0',
102
- resources: { customers: { select: ['id', 'name'] } },
105
+ name: "crm",
106
+ version: "0.1.0",
107
+ resources: { customers: { select: ["id", "name"] } },
103
108
  });
104
109
 
105
110
  Deno.serve(mcp.fetch);
@@ -11,7 +11,8 @@ supabase-js, and don't use the service role for anything a user does.
11
11
  ## Workflow: test a table or policy
12
12
 
13
13
  1. Start the stack (`supabase start`) and write `.env.local` with
14
- `better-supabase env`.
14
+ `better-supabase env`. In a sandbox without Docker, start it with
15
+ `SUPABASE_EXPERIMENTAL_STACK=1` (the Supabase CLI's native stack).
15
16
  2. Add the rows the test needs to `supabase/seed.ts`, including one row
16
17
  owned by another tenant, then `supabase db reset`.
17
18
  3. Write the RLS test with `asUser` for each role that matters.
@@ -25,27 +26,30 @@ again once you restore it.
25
26
  ## Setup
26
27
 
27
28
  - `better-supabase env` writes the URL and keys to `.env.local`; load it in the test setup.
29
+ - `better-supabase keys` creates `supabase/signing_keys.json`. Set `signing_keys_path = "./signing_keys.json"` under `[auth]` in `supabase/config.toml`, gitignore the file and restart the stack. `asUser` signs ES256 tokens with it.
28
30
  - Typed fixtures live in `supabase/seed.ts`:
29
31
 
30
32
  ```ts
31
- import { defineSeed } from 'better-supabase/testing';
32
- import { sb } from '../src/lib/supabase.ts';
33
+ import { defineSeed } from "better-supabase/testing";
34
+ import { sb } from "../src/lib/supabase.ts";
33
35
 
34
36
  export const seed = defineSeed(sb, {
35
- organizations: { acme: { id: ACME, name: 'Acme' } },
36
- customers: { first: { id: FIRST, organizationId: ACME, name: 'First' } },
37
+ organizations: { acme: { id: ACME, name: "Acme" } },
38
+ customers: { first: { id: FIRST, organizationId: ACME, name: "First" } },
37
39
  });
38
40
  ```
39
41
 
40
- `better-supabase seed` renders them to SQL for `supabase db reset`, and
41
- tests import the same rows (`seed.rows.customers.first.id`).
42
+ `better-supabase seed` renders them to SQL for `supabase db reset`, and
43
+ tests import the same rows (`seed.rows.customers.first.id`).
42
44
 
43
45
  ## RLS tests
44
46
 
45
47
  ```ts
46
- const alice = await asUser(sb, { sub: aliceId, org_id: ACME }, { postgres });
48
+ const alice = await asUser(sb, { sub: aliceId, tenant_id: ACME }, { postgres });
47
49
  expect(await alice.db.customers.count().orThrow()).toBe(1);
48
- expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok: false });
50
+ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({
51
+ ok: false,
52
+ });
49
53
  ```
50
54
 
51
55
  - Check both directions: the user sees their own rows and never sees another tenant's.
@@ -54,8 +58,8 @@ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok
54
58
 
55
59
  ## API tests
56
60
 
57
- Pass `auth: { resolvers: [localAuth(LOCAL_JWT_SECRET)] }` to the adapter and
58
- send `authorization: Bearer ${alice.token}`.
61
+ Send `authorization: Bearer ${alice.token}` (or a token from `signLocalJwt`).
62
+ The adapter verifies it against the local JWKS; no test-only resolver is needed.
59
63
 
60
64
  ## pgTAP
61
65
 
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- // Checked in so pnpm can link the bin on a fresh install, before dist/ is built.
3
- import '../dist/cli/bin.js';
@@ -1,14 +0,0 @@
1
- //#region src/casing/index.ts
2
- /** `first_name` to `firstName`. Leaves already camel-cased names untouched. */
3
- function toCamel(name) {
4
- return name.replace(/_+([a-z0-9])/g, (_, char) => char.toUpperCase());
5
- }
6
- /** `firstName` to `first_name`. */
7
- function toSnake(name) {
8
- return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
9
- }
10
- function applyCasing(name, casing) {
11
- return casing === "camel" ? toCamel(name) : name;
12
- }
13
- //#endregion
14
- export { toCamel as n, toSnake as r, applyCasing as t };
package/dist/cli/bin.d.ts DELETED
@@ -1 +0,0 @@
1
- export {}