better-supabase 0.2.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 (139) hide show
  1. package/README.md +34 -31
  2. package/dist/base64-CQeB1eSf.js +42 -0
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +4 -14
  5. package/dist/{compiler-CZ9nIuqy.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
  6. package/dist/config/index.d.ts +4 -3
  7. package/dist/config/index.js +5 -3
  8. package/dist/config-Be4GwsqI.js +129 -0
  9. package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
  10. package/dist/{config-ClDXmVtY.js → config-DOsGYSiT.js} +14 -137
  11. package/dist/{define-BXMYD9AY.js → define-DF341GDj.js} +71 -51
  12. package/dist/{define-e3zI4_4g.d.ts → define-DUbydmJa.d.ts} +10 -9
  13. package/dist/edge/index.d.ts +4 -4
  14. package/dist/edge/index.js +4 -4
  15. package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-DvJXoI02.d.ts} +1 -1
  16. package/dist/env/index.js +203 -1
  17. package/dist/events/index.d.ts +1 -1
  18. package/dist/events/index.js +139 -1
  19. package/dist/{events-DOt3nSO-.d.ts → events-Do8jXh4D.d.ts} +3 -3
  20. package/dist/{executor-BYXQ9AGn.d.ts → executor-B26lgbHD.d.ts} +2 -2
  21. package/dist/{executor-DeppvirK.js → executor-C57jx2pn.js} +7 -2
  22. package/dist/{executor-CGdNQ27g.d.ts → executor-DynmIWhq.d.ts} +2 -2
  23. package/dist/hono/index.d.ts +5 -5
  24. package/dist/hono/index.js +4 -4
  25. package/dist/{index-CBDufH1M.d.ts → index-BNsABq7P.d.ts} +8 -8
  26. package/dist/{index-Bw1bwtC0.d.ts → index-CADlNHK-.d.ts} +27 -14
  27. package/dist/{index-DuP14VHo.d.ts → index-CTTMS6_1.d.ts} +19 -8
  28. package/dist/index-Cf-D2jk1.d.ts +50 -0
  29. package/dist/{index-K-3WM2Hp.d.ts → index-CriFFJ_p.d.ts} +4 -4
  30. package/dist/{index-Ciu5Xlml.d.ts → index-GV77NpRP.d.ts} +5 -4
  31. package/dist/{index-D4v7sncu.d.ts → index-UJ-LUxtI.d.ts} +3 -3
  32. package/dist/{index-CG69s15v.d.ts → index-kpgopLNv.d.ts} +4 -3
  33. package/dist/{index-DbxFr7XG.d.ts → index-qriRM9cZ.d.ts} +7 -2
  34. package/dist/index.d.ts +12 -12
  35. package/dist/index.js +16 -7
  36. package/dist/jobs/index.d.ts +25 -24
  37. package/dist/jobs/index.js +16 -10
  38. package/dist/{json-schema-BpYqkCKf.js → json-schema--aOXAGfU.js} +7 -2
  39. package/dist/lint/index.js +1 -1
  40. package/dist/list/index.d.ts +2 -2
  41. package/dist/list/index.js +128 -93
  42. package/dist/{live-DJyZMD6w.d.ts → live-QE3UPbGm.d.ts} +16 -6
  43. package/dist/mcp/index.d.ts +24 -17
  44. package/dist/mcp/index.js +41 -12
  45. package/dist/{mfa-CpLfJSde.js → mfa-BxoR1RkN.js} +74 -19
  46. package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
  47. package/dist/next/index.d.ts +39 -26
  48. package/dist/next/index.js +28 -23
  49. package/dist/openapi/index.d.ts +1 -1
  50. package/dist/openapi/index.js +57 -34
  51. package/dist/orpc/index.d.ts +6 -6
  52. package/dist/orpc/index.js +3 -3
  53. package/dist/otel/index.d.ts +2 -2
  54. package/dist/otel/index.js +4 -2
  55. package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-Bta_G0v-.js} +11 -6
  56. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  57. package/dist/{plugin-C4TR9J0Z.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
  58. package/dist/plugins/actor/index.d.ts +1 -1
  59. package/dist/plugins/rules/index.d.ts +2 -2
  60. package/dist/plugins/rules/index.js +4 -4
  61. package/dist/plugins/soft-delete/index.d.ts +3 -3
  62. package/dist/plugins/soft-delete/index.js +2 -2
  63. package/dist/plugins/tenant/index.d.ts +2 -2
  64. package/dist/plugins/timestamps/index.d.ts +1 -1
  65. package/dist/plugins/timestamps/index.js +1 -1
  66. package/dist/plugins/validation/index.d.ts +2 -2
  67. package/dist/pool-B9qsLyFR.d.ts +71 -0
  68. package/dist/postgres/index.d.ts +10 -5
  69. package/dist/postgres/index.js +36 -16
  70. package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
  71. package/dist/{problem-BTkTtRV9.js → problem-CGt1ADix.js} +10 -4
  72. package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
  73. package/dist/query/index.d.ts +2 -2
  74. package/dist/query/index.js +1 -1
  75. package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
  76. package/dist/react/index.d.ts +7 -7
  77. package/dist/react/index.js +2 -2
  78. package/dist/react/server.d.ts +3 -3
  79. package/dist/react/server.js +1 -1
  80. package/dist/react/session.d.ts +21 -2
  81. package/dist/react/session.js +19 -1
  82. package/dist/{react-C83BdBGt.js → react-DNBYNazu.js} +20 -16
  83. package/dist/{read-set-4lI3W_gZ.js → read-set-D0qBcboM.js} +3 -3
  84. package/dist/realtime/index.d.ts +4 -3
  85. package/dist/realtime/index.js +7 -6
  86. package/dist/{resolve-CjHILk2w.d.ts → resolve-BRciX2jK.d.ts} +6 -5
  87. package/dist/{resource-CgZa3YG_.d.ts → resource-CwVgiI5q.d.ts} +16 -7
  88. package/dist/{resource-BHEWmQJK.js → resource-D30rp0GN.js} +38 -16
  89. package/dist/{respond-DMcbQwfN.js → respond-C44EZAJo.js} +35 -19
  90. package/dist/{respond-Dne1ARSa.d.ts → respond-DAkPIdTH.d.ts} +15 -9
  91. package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
  92. package/dist/server/index.d.ts +35 -10
  93. package/dist/server/index.js +38 -9
  94. package/dist/{simplify-bwJpLnaS.js → simplify-LakqK1vH.js} +10 -16
  95. package/dist/sql/index.d.ts +139 -0
  96. package/dist/sql/index.js +1983 -0
  97. package/dist/{sql-DTjA1LPj.js → sql-BIgk12ew.js} +72 -16
  98. package/dist/ssr/index.d.ts +4 -4
  99. package/dist/ssr/index.js +2 -2
  100. package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
  101. package/dist/{stats-C5W-rBfR.js → stats-D78UQ8Yu.js} +8 -7
  102. package/dist/storage/index.d.ts +4 -3
  103. package/dist/storage/index.js +530 -1
  104. package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
  105. package/dist/temporal-BwshQk0W.js +27 -0
  106. package/dist/temporal-required-CMxeSVPo.js +19 -0
  107. package/dist/testing/index.d.ts +13 -11
  108. package/dist/testing/index.js +128 -29
  109. package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
  110. package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
  111. package/dist/{view-D1s0wa0T.d.ts → view-3Po_PVlr.d.ts} +34 -4
  112. package/dist/{view-aOYizTeE.js → view-wZ-9qIUz.js} +11 -2
  113. package/dist/webhooks/index.d.ts +18 -16
  114. package/dist/webhooks/index.js +1 -1
  115. package/dist/{webhooks-DMipjhSP.js → webhooks-CAIFQSTM.js} +11 -9
  116. package/package.json +24 -28
  117. package/schemas/config-v1.json +31 -1
  118. package/schemas/doctor-report-v1.json +17 -1
  119. package/schemas/snapshot-v2.json +35 -0
  120. package/skills/better-supabase/SKILL.md +23 -11
  121. package/skills/better-supabase/references/plugins.md +21 -18
  122. package/skills/better-supabase/references/troubleshooting.md +15 -15
  123. package/skills/better-supabase-api/SKILL.md +7 -7
  124. package/skills/better-supabase-api/references/adapters.md +30 -25
  125. package/skills/better-supabase-testing/SKILL.md +11 -8
  126. package/bin/better-supabase.js +0 -4
  127. package/dist/casing-da0uRTqt.js +0 -14
  128. package/dist/cli/bin.d.ts +0 -1
  129. package/dist/cli/bin.js +0 -15
  130. package/dist/cli/index.d.ts +0 -226
  131. package/dist/cli/index.js +0 -3
  132. package/dist/cli-DrSgDOQC.js +0 -6646
  133. package/dist/env-BEToab0I.js +0 -204
  134. package/dist/events-BuzTgn2D.js +0 -135
  135. package/dist/pool-R2m_DkXi.d.ts +0 -37
  136. package/dist/seed-CzecODcJ.js +0 -101
  137. package/dist/session-C3LxnQ0e.d.ts +0 -22
  138. package/dist/session-CqEwzVup.js +0 -20
  139. package/dist/storage-DuGn38fr.js +0 -527
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "better-supabase",
3
- "version": "0.2.0",
4
- "description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue and framework integrations, with a CLI from day one.",
3
+ "version": "0.3.0",
4
+ "description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue and framework integrations. The CLI ships as @better-supabase/cli.",
5
5
  "keywords": [
6
6
  "hono",
7
7
  "mcp",
@@ -25,11 +25,7 @@
25
25
  "url": "git+https://github.com/ScaleDockHQ/better-supabase.git",
26
26
  "directory": "packages/better-supabase"
27
27
  },
28
- "bin": {
29
- "better-supabase": "./bin/better-supabase.js"
30
- },
31
28
  "files": [
32
- "bin",
33
29
  "dist",
34
30
  "schemas",
35
31
  "skills",
@@ -160,14 +156,14 @@
160
156
  "types": "./dist/plugins/validation/index.d.ts",
161
157
  "default": "./dist/plugins/validation/index.js"
162
158
  },
159
+ "./sql": {
160
+ "types": "./dist/sql/index.d.ts",
161
+ "default": "./dist/sql/index.js"
162
+ },
163
163
  "./testing": {
164
164
  "types": "./dist/testing/index.d.ts",
165
165
  "default": "./dist/testing/index.js"
166
166
  },
167
- "./cli": {
168
- "types": "./dist/cli/index.d.ts",
169
- "default": "./dist/cli/index.js"
170
- },
171
167
  "./schemas/*.json": "./schemas/*.json",
172
168
  "./package.json": "./package.json"
173
169
  },
@@ -177,19 +173,17 @@
177
173
  },
178
174
  "dependencies": {
179
175
  "@standard-schema/spec": "1.1.0",
180
- "@supabase/middleware": "0.6.0",
181
- "@supabase/postgrest-typegen": "0.2.2",
182
- "@supabase/server": "1.8.1",
176
+ "@supabase/middleware": "1.0.0",
177
+ "@supabase/postgrest-typegen": "0.3.1",
178
+ "@supabase/server": "1.9.0",
183
179
  "@supabase/ssr": "0.12.7"
184
180
  },
185
181
  "devDependencies": {
186
182
  "@arethetypeswrong/cli": "0.18.5",
187
183
  "@better-supabase/ox-config": "0.0.0",
188
184
  "@better-supabase/typescript-config": "0.0.0",
189
- "@effect/platform-node": "4.0.0-rc.117",
190
185
  "@opentelemetry/api": "1.9.1",
191
186
  "@orpc/server": "2.0.0-beta.40",
192
- "@supabase/config": "0.10.0",
193
187
  "@supabase/supabase-js": "2.117.2",
194
188
  "@tanstack/query-core": "5.104.0",
195
189
  "@tanstack/react-query": "5.104.0",
@@ -198,32 +192,33 @@
198
192
  "@types/react": "19.3.0",
199
193
  "@types/react-dom": "19.3.0",
200
194
  "@valibot/to-json-schema": "1.8.0",
201
- "@vitest/coverage-v8": "5.0.2",
202
- "effect": "4.0.0-rc.117",
203
- "hono": "4.13.10",
204
- "next": "16.3.7",
205
- "oxfmt": "0.71.0",
195
+ "@vitest/coverage-v8": "5.0.3",
196
+ "ajv": "8.20.0",
197
+ "ajv-formats": "3.0.1",
198
+ "hono": "4.13.12",
199
+ "next": "16.3.8",
206
200
  "oxlint": "1.86.0",
207
- "pg": "8.23.0",
201
+ "pg": "8.23.1",
208
202
  "publint": "0.3.24",
209
203
  "react": "19.3.0",
210
204
  "react-dom": "19.3.0",
205
+ "temporal-polyfill": "1.0.5",
211
206
  "tsdown": "0.23.0",
212
207
  "typescript": "7.0.2",
213
208
  "valibot": "1.5.0",
214
- "vitest": "5.0.2",
209
+ "vitest": "5.0.3",
215
210
  "zod": "4.6.5"
216
211
  },
217
212
  "peerDependencies": {
218
213
  "@opentelemetry/api": ">=1.9",
219
214
  "@orpc/server": ">=2.0.0-0",
220
- "@supabase/config": ">=0.9.0 <0.11",
221
215
  "@supabase/supabase-js": "^2.116.0",
222
216
  "@tanstack/query-core": "^5",
223
217
  "hono": ">=4",
224
218
  "next": ">=16",
225
219
  "pg": ">=8",
226
- "react": "^19"
220
+ "react": "^19",
221
+ "temporal-polyfill": ">=1"
227
222
  },
228
223
  "peerDependenciesMeta": {
229
224
  "@opentelemetry/api": {
@@ -232,9 +227,6 @@
232
227
  "@orpc/server": {
233
228
  "optional": true
234
229
  },
235
- "@supabase/config": {
236
- "optional": true
237
- },
238
230
  "@tanstack/query-core": {
239
231
  "optional": true
240
232
  },
@@ -249,6 +241,9 @@
249
241
  },
250
242
  "react": {
251
243
  "optional": true
244
+ },
245
+ "temporal-polyfill": {
246
+ "optional": true
252
247
  }
253
248
  },
254
249
  "engines": {
@@ -260,6 +255,7 @@
260
255
  "check:publish": "publint && pnpm pack --out pack.tgz && attw --profile esm-only pack.tgz && rm -f pack.tgz",
261
256
  "lint": "oxlint",
262
257
  "test": "vitest run --coverage",
263
- "typecheck": "tsc --noEmit --checkers 4"
258
+ "transit": "true",
259
+ "typecheck": "tsc --noEmit"
264
260
  }
265
261
  }
@@ -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
  }
@@ -283,6 +283,36 @@
283
283
  "description": "The tenant id column of that table.",
284
284
  "type": "string",
285
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"
286
316
  }
287
317
  }
288
318
  },
@@ -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",
@@ -65,6 +80,7 @@
65
80
  "BS405",
66
81
  "BS406",
67
82
  "BS407",
83
+ "BS408",
68
84
  "BS501",
69
85
  "BS502"
70
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
  }
@@ -901,6 +907,12 @@
901
907
  },
902
908
  "partial": {
903
909
  "type": "boolean"
910
+ },
911
+ "method": {
912
+ "type": "string"
913
+ },
914
+ "predicate": {
915
+ "type": ["string", "null"]
904
916
  }
905
917
  }
906
918
  }
@@ -996,6 +1008,29 @@
996
1008
  }
997
1009
  }
998
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
+ },
999
1034
  "large": {
1000
1035
  "description": "Postgres estimates at least 10,000 rows; the unbounded-read lint rule reads it.",
1001
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
 
@@ -59,7 +59,10 @@ Never read it from `user_metadata`, a URL or a request body. When the project
59
59
  has a `permdock.config.ts`, PermDock's hook writes that claim and the
60
60
  memberships; don't run `sql add tenant` there or call `membership_claims()`
61
61
  from a hook. `sql add entitlements` is fine: `features` is not a PermDock
62
- claim, but PermDock's hook doesn't write it yet, so read entitlements from the
63
- database on the server instead of `hasEntitlement(session, ...)`.
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`.
64
67
 
65
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
 
@@ -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.
@@ -29,24 +30,26 @@ again once you restore it.
29
30
  - Typed fixtures live in `supabase/seed.ts`:
30
31
 
31
32
  ```ts
32
- import { defineSeed } from 'better-supabase/testing';
33
- import { sb } from '../src/lib/supabase.ts';
33
+ import { defineSeed } from "better-supabase/testing";
34
+ import { sb } from "../src/lib/supabase.ts";
34
35
 
35
36
  export const seed = defineSeed(sb, {
36
- organizations: { acme: { id: ACME, name: 'Acme' } },
37
- customers: { first: { id: FIRST, organizationId: ACME, name: 'First' } },
37
+ organizations: { acme: { id: ACME, name: "Acme" } },
38
+ customers: { first: { id: FIRST, organizationId: ACME, name: "First" } },
38
39
  });
39
40
  ```
40
41
 
41
- `better-supabase seed` renders them to SQL for `supabase db reset`, and
42
- 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`).
43
44
 
44
45
  ## RLS tests
45
46
 
46
47
  ```ts
47
48
  const alice = await asUser(sb, { sub: aliceId, tenant_id: ACME }, { postgres });
48
49
  expect(await alice.db.customers.count().orThrow()).toBe(1);
49
- 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
+ });
50
53
  ```
51
54
 
52
55
  - Check both directions: the user sees their own rows and never sees another tenant's.
@@ -1,4 +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
- // oxlint-disable-next-line import/no-unassigned-import -- the CLI module runs on import.
4
- import "../dist/cli/bin.js";