better-supabase 0.3.0 → 0.4.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 (172) hide show
  1. package/README.md +7 -6
  2. package/bin/better-supabase.js +4 -0
  3. package/dist/{base64-CQeB1eSf.js → base64-D2gsioCY.js} +2 -2
  4. package/dist/claims-BAf6c7is.js +48 -0
  5. package/dist/cli/_format-Cax5EsM9.js +101 -0
  6. package/dist/cli/bin.d.ts +1 -0
  7. package/dist/cli/bin.js +48 -0
  8. package/dist/cli/command-CK14fP91.js +502 -0
  9. package/dist/cli/compare-BmIWVEui.js +9 -0
  10. package/dist/cli/config-D9R0o1LH.js +1895 -0
  11. package/dist/cli/config-SSKSTUl2.js +2 -0
  12. package/dist/cli/db-DOBPt660.js +52 -0
  13. package/dist/cli/dist-B1MvLv-M.js +129 -0
  14. package/dist/cli/dist-CFgGknmZ.js +1112 -0
  15. package/dist/cli/dist-Du7VnEUG.js +621 -0
  16. package/dist/cli/doctor-BOym3mbL.js +2382 -0
  17. package/dist/cli/env-BZ0uy8XK.js +108 -0
  18. package/dist/cli/env-jIFUADLY.js +511 -0
  19. package/dist/cli/errors-4v4DTtda.js +56 -0
  20. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  21. package/dist/cli/gen-Ccn_p90L.js +2008 -0
  22. package/dist/cli/gen-D5cLCULx.js +2 -0
  23. package/dist/cli/index.d.ts +493 -0
  24. package/dist/cli/index.js +184 -0
  25. package/dist/cli/init-ClnxRYBF.js +550 -0
  26. package/dist/cli/introspect-DxqfkVIv.js +81 -0
  27. package/dist/cli/io-k41_z-jT.js +24 -0
  28. package/dist/cli/json5-ChRycquj.js +787 -0
  29. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  30. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  31. package/dist/cli/keys-ByKHnvP9.js +83 -0
  32. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  33. package/dist/cli/openapi-8WHjxiTJ.js +66 -0
  34. package/dist/cli/project-B_GeXyHh.js +70 -0
  35. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  36. package/dist/cli/prompts-BTYQGunc.js +2 -0
  37. package/dist/cli/read-sets-CMa29t9a.js +971 -0
  38. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  39. package/dist/cli/run-CP20LKk_.js +347 -0
  40. package/dist/cli/seed-5J6eu_Xg.js +98 -0
  41. package/dist/cli/skills-LJJIhLFR.js +137 -0
  42. package/dist/cli/sql-D4VbqmXi.js +199 -0
  43. package/dist/cli/style-C0NFdJRL.js +29 -0
  44. package/dist/cli/toml-DB-MNnFs.js +424 -0
  45. package/dist/cli/version-5mW_8sY1.js +4 -0
  46. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  47. package/dist/client/index.d.ts +2 -2
  48. package/dist/client/index.js +13 -13
  49. package/dist/{compiler-2zqGzxVZ.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
  50. package/dist/config/index.js +4 -4
  51. package/dist/{config-Be4GwsqI.js → config-BpxDfnbg.js} +1 -1
  52. package/dist/{config-DOsGYSiT.js → config-CbvFmGD8.js} +1 -1
  53. package/dist/{define-DF341GDj.js → define-Bh0AqTik.js} +160 -79
  54. package/dist/{define-DUbydmJa.d.ts → define-CRS2MRV3.d.ts} +32 -28
  55. package/dist/edge/index.d.ts +8 -12
  56. package/dist/edge/index.js +50 -24
  57. package/dist/{entitlements-DvJXoI02.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
  58. package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
  59. package/dist/events/index.d.ts +1 -1
  60. package/dist/events/index.js +4 -4
  61. package/dist/{events-Do8jXh4D.d.ts → events-B21UOhvP.d.ts} +4 -4
  62. package/dist/{executor-DynmIWhq.d.ts → executor-B5IoUxLE.d.ts} +1 -1
  63. package/dist/{executor-C57jx2pn.js → executor-CVt6dD0B.js} +3 -3
  64. package/dist/{executor-B26lgbHD.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
  65. package/dist/hono/index.d.ts +16 -23
  66. package/dist/hono/index.js +10 -10
  67. package/dist/{index-CTTMS6_1.d.ts → index-528Ugmyp.d.ts} +12 -3
  68. package/dist/{index-CADlNHK-.d.ts → index-BAy4tsAP.d.ts} +5 -5
  69. package/dist/{index-BNsABq7P.d.ts → index-BBdvo-5d.d.ts} +23 -25
  70. package/dist/{index-qriRM9cZ.d.ts → index-BhHEC04r.d.ts} +3 -3
  71. package/dist/{index-GV77NpRP.d.ts → index-D5IfUIDh.d.ts} +3 -3
  72. package/dist/{index-CriFFJ_p.d.ts → index-DFmOpBGC.d.ts} +11 -11
  73. package/dist/{index-UJ-LUxtI.d.ts → index-RlLXd8dL.d.ts} +12 -12
  74. package/dist/index.d.ts +15 -15
  75. package/dist/index.js +13 -13
  76. package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
  77. package/dist/jobs/index.d.ts +10 -4
  78. package/dist/jobs/index.js +32 -16
  79. package/dist/{json-schema--aOXAGfU.js → json-schema-DP6V7cbr.js} +1 -1
  80. package/dist/list/index.d.ts +1 -1
  81. package/dist/list/index.js +3 -3
  82. package/dist/{live-QE3UPbGm.d.ts → live-BHakSWWo.d.ts} +12 -12
  83. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  84. package/dist/lookup-BaP5bI27.js +18 -0
  85. package/dist/mcp/index.d.ts +18 -18
  86. package/dist/mcp/index.js +23 -18
  87. package/dist/{mfa-x3V3vGh-.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
  88. package/dist/{mfa-BxoR1RkN.js → mfa-byfjruR9.js} +118 -29
  89. package/dist/next/image/index.js +6 -3
  90. package/dist/next/index.d.ts +38 -35
  91. package/dist/next/index.js +41 -33
  92. package/dist/openapi/index.d.ts +1 -1
  93. package/dist/openapi/index.js +6 -6
  94. package/dist/orpc/index.d.ts +17 -20
  95. package/dist/orpc/index.js +11 -11
  96. package/dist/otel/index.d.ts +5 -5
  97. package/dist/otel/index.js +81 -62
  98. package/dist/{permdock-sql-Bta_G0v-.js → permdock-sql-C4KFzLc9.js} +1 -1
  99. package/dist/{plugin-DVRB8qNE.d.ts → plugin-bNhvJwRh.d.ts} +2 -2
  100. package/dist/plugins/actor/index.d.ts +1 -1
  101. package/dist/plugins/actor/index.js +1 -1
  102. package/dist/plugins/rules/index.d.ts +2 -2
  103. package/dist/plugins/rules/index.js +42 -12
  104. package/dist/plugins/soft-delete/index.d.ts +2 -2
  105. package/dist/plugins/soft-delete/index.js +2 -2
  106. package/dist/plugins/tenant/index.d.ts +2 -2
  107. package/dist/plugins/tenant/index.js +10 -4
  108. package/dist/plugins/timestamps/index.d.ts +1 -1
  109. package/dist/plugins/timestamps/index.js +1 -1
  110. package/dist/plugins/validation/index.d.ts +2 -2
  111. package/dist/plugins/validation/index.js +3 -3
  112. package/dist/{pool-B9qsLyFR.d.ts → pool-DFwEQwmN.d.ts} +11 -4
  113. package/dist/postgres/index.d.ts +5 -5
  114. package/dist/postgres/index.js +3 -2
  115. package/dist/{postgrest-DmBlevh-.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
  116. package/dist/{problem-CGt1ADix.js → problem-BihjPR0t.js} +1 -1
  117. package/dist/{problem-Z2RTQjZD.d.ts → problem-U8UEKccv.d.ts} +1 -1
  118. package/dist/query/index.d.ts +2 -2
  119. package/dist/query/index.js +2 -2
  120. package/dist/{query-BdKLTPlj.js → query-BMMSWbI7.js} +18 -18
  121. package/dist/react/index.d.ts +6 -6
  122. package/dist/react/index.js +1 -1
  123. package/dist/react/server.d.ts +5 -5
  124. package/dist/react/server.js +3 -3
  125. package/dist/react/session.d.ts +4 -4
  126. package/dist/react/session.js +2 -2
  127. package/dist/{react-DNBYNazu.js → react-CQHMaESR.js} +39 -39
  128. package/dist/{read-set-D0qBcboM.js → read-set-FWK8qQ58.js} +50 -15
  129. package/dist/realtime/index.d.ts +1 -1
  130. package/dist/realtime/index.js +13 -12
  131. package/dist/{index-kpgopLNv.d.ts → refresh-BwYkyNOQ.d.ts} +6 -3
  132. package/dist/{resolve-BRciX2jK.d.ts → resolve-BAqQcwMW.d.ts} +15 -10
  133. package/dist/{resource-CwVgiI5q.d.ts → resource-BJcaIfxi.d.ts} +5 -5
  134. package/dist/{resource-D30rp0GN.js → resource-My_y-Syh.js} +6 -6
  135. package/dist/{respond-DAkPIdTH.d.ts → respond-DgIbdrt8.d.ts} +31 -12
  136. package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
  137. package/dist/{result-c2OYgCV2.d.ts → result-CqEsbgYs.d.ts} +3 -3
  138. package/dist/server/index.d.ts +14 -14
  139. package/dist/server/index.js +12 -12
  140. package/dist/{respond-C44EZAJo.js → server-BA75B0mT.js} +139 -100
  141. package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
  142. package/dist/{simplify-LakqK1vH.js → simplify-Cqvfr9dj.js} +67 -24
  143. package/dist/sql/index.d.ts +1 -1
  144. package/dist/sql/index.js +7 -7
  145. package/dist/{sql-BIgk12ew.js → sql-B34HU2DI.js} +3 -2
  146. package/dist/ssr/index.d.ts +4 -4
  147. package/dist/ssr/index.js +2 -2
  148. package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
  149. package/dist/{standard-eudBiRk1.d.ts → standard-BsaCYQJc.d.ts} +1 -1
  150. package/dist/{stats-D78UQ8Yu.js → stats-C_YxA2r1.js} +33 -8
  151. package/dist/storage/index.d.ts +2 -2
  152. package/dist/storage/index.js +26 -8
  153. package/dist/{tables-Dhha3BXM.js → tables-Cb-3_VWc.js} +14 -0
  154. package/dist/{temporal-required-CMxeSVPo.js → temporal-required-CBNHT6ni.js} +1 -1
  155. package/dist/testing/index.d.ts +20 -21
  156. package/dist/testing/index.js +45 -45
  157. package/dist/{version-BfpG2gks.js → version-Bmf7_FzM.js} +1 -1
  158. package/dist/{view-3Po_PVlr.d.ts → view-B64o81wp.d.ts} +5 -5
  159. package/dist/{view-wZ-9qIUz.js → view-BrJI2LKp.js} +1 -1
  160. package/dist/webhooks/index.d.ts +3 -3
  161. package/dist/webhooks/index.js +1 -1
  162. package/dist/{webhooks-CAIFQSTM.js → webhooks-Bh_Jf_-l.js} +37 -18
  163. package/package.json +36 -4
  164. package/skills/better-supabase/SKILL.md +57 -10
  165. package/skills/better-supabase/references/plugins.md +7 -11
  166. package/skills/better-supabase/references/troubleshooting.md +24 -4
  167. package/skills/better-supabase-api/SKILL.md +66 -18
  168. package/skills/better-supabase-api/references/adapters.md +44 -26
  169. package/skills/better-supabase-auth/SKILL.md +146 -0
  170. package/skills/better-supabase-auth/references/permdock.md +55 -0
  171. package/skills/better-supabase-testing/SKILL.md +58 -4
  172. package/dist/claims-CoTobNC-.js +0 -25
@@ -1,11 +1,11 @@
1
- import { n as dbError } from "./errors-Bl3rkOzo.js";
1
+ import { n as dbError } from "./errors-L0xK41y_.js";
2
2
  import { n as nowInstant, r as optionalTemporal } from "./temporal-BwshQk0W.js";
3
- import { n as temporalMissing } from "./temporal-required-CMxeSVPo.js";
4
- import { i as ok, n as err } from "./result-DWXatkd6.js";
5
- import { i as toBase64, r as fromBase64 } from "./base64-CQeB1eSf.js";
6
- import { s as toApp } from "./shared-_6-uPL7E.js";
3
+ import { n as temporalMissing } from "./temporal-required-CBNHT6ni.js";
4
+ import { i as ok, n as err } from "./result-Bj6yt33D.js";
5
+ import { i as toBase64, r as fromBase64 } from "./base64-D2gsioCY.js";
6
+ import { s as toApp } from "./shared-DnoCdMDr.js";
7
7
  //#region src/webhooks/verify.ts
8
- const encoder = new TextEncoder();
8
+ const encoder = /* @__PURE__ */ new TextEncoder();
9
9
  /** Accepts `whsec_…`, `v1,whsec_…` (Supabase Auth hooks) or raw base64. */
10
10
  function secretBytes(secret) {
11
11
  const raw = secret.replace(/^v1,/, "").replace(/^whsec_/, "");
@@ -15,12 +15,31 @@ function secretBytes(secret) {
15
15
  throw new TypeError("Webhook secret is not valid base64");
16
16
  }
17
17
  }
18
+ /** Secrets come from configuration, so a handful of keys suffice. */
19
+ const keys = /* @__PURE__ */ new Map();
20
+ function keyFor(secret) {
21
+ let key = keys.get(secret);
22
+ if (!key) {
23
+ key = crypto.subtle.importKey("raw", secretBytes(secret), {
24
+ name: "HMAC",
25
+ hash: "SHA-256"
26
+ }, false, ["sign", "verify"]);
27
+ if (keys.size < 16) {
28
+ keys.set(secret, key);
29
+ key.catch(() => keys.delete(secret));
30
+ }
31
+ }
32
+ return key;
33
+ }
18
34
  async function signature(secret, content) {
19
- const key = await crypto.subtle.importKey("raw", secretBytes(secret), {
20
- name: "HMAC",
21
- hash: "SHA-256"
22
- }, false, ["sign"]);
23
- return toBase64(new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(content))));
35
+ return toBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await keyFor(secret), encoder.encode(content))));
36
+ }
37
+ function decodeSignature(value) {
38
+ try {
39
+ return fromBase64(value);
40
+ } catch {
41
+ return;
42
+ }
24
43
  }
25
44
  /** Constant-time string comparison. */
26
45
  function timingSafeEqual(a, b) {
@@ -51,7 +70,6 @@ async function signWebhook(secret, message) {
51
70
  */
52
71
  async function verifyWebhook(input, secrets, options = {}) {
53
72
  const headers = input.headers;
54
- const body = input instanceof Request ? await input.text() : input.body;
55
73
  const id = header(headers, "webhook-id");
56
74
  const timestamp = header(headers, "webhook-timestamp");
57
75
  const signatures = header(headers, "webhook-signature");
@@ -64,12 +82,13 @@ async function verifyWebhook(input, secrets, options = {}) {
64
82
  const tolerance = options.tolerance ?? 300;
65
83
  if (seconds < now - tolerance) return err(unauthorized("WEBHOOK_TIMESTAMP_TOO_OLD", "Webhook timestamp is too old"));
66
84
  if (seconds > now + tolerance) return err(unauthorized("WEBHOOK_TIMESTAMP_TOO_NEW", "Webhook timestamp is in the future"));
67
- const content = `${id}.${timestamp}.${body}`;
68
- const offered = signatures.split(" ").map((entry) => entry.split(",")).filter(([version, value]) => version === "v1" && value).map(([, value]) => value);
85
+ const body = input instanceof Request ? await input.text() : input.body;
86
+ const content = encoder.encode(`${id}.${timestamp}.${body}`);
87
+ const offered = signatures.split(" ").map((entry) => entry.split(",")).filter(([version, value]) => version === "v1" && value).flatMap(([, value]) => decodeSignature(value) ?? []);
69
88
  let valid = false;
70
89
  for (const secret of typeof secrets === "string" ? [secrets] : secrets) {
71
- const expected = await signature(secret, content);
72
- for (const candidate of offered) if (timingSafeEqual(candidate, expected)) valid = true;
90
+ const key = await keyFor(secret);
91
+ for (const candidate of offered) if (await crypto.subtle.verify("HMAC", key, candidate, content)) valid = true;
73
92
  }
74
93
  if (!valid) return err(unauthorized("WEBHOOK_INVALID_SIGNATURE", "Invalid webhook signature"));
75
94
  try {
@@ -127,8 +146,8 @@ function isDatabaseWebhook(value) {
127
146
  * Reads a database webhook payload for `table` as an app-cased change.
128
147
  * Returns `null` for other tables or payloads that aren't database webhooks.
129
148
  */
130
- function databaseChange(sb, table, payload) {
131
- const meta = sb.meta.tables[table];
149
+ function databaseChange(betterSupabase, table, payload) {
150
+ const meta = betterSupabase.meta.tables[table];
132
151
  if (!meta || !isDatabaseWebhook(payload) || payload.table !== meta.name || payload.schema !== meta.schema) return null;
133
152
  return {
134
153
  type: payload.type,
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "better-supabase",
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.",
3
+ "version": "0.4.0",
4
+ "description": "Strongly typed Supabase: generated schema, typed repositories, plugins, auth glue, framework integrations and the better-supabase CLI.",
5
5
  "keywords": [
6
+ "cli",
7
+ "codegen",
6
8
  "hono",
7
9
  "mcp",
8
10
  "nextjs",
@@ -25,7 +27,11 @@
25
27
  "url": "git+https://github.com/ScaleDockHQ/better-supabase.git",
26
28
  "directory": "packages/better-supabase"
27
29
  },
30
+ "bin": {
31
+ "better-supabase": "./bin/better-supabase.js"
32
+ },
28
33
  "files": [
34
+ "bin",
29
35
  "dist",
30
36
  "schemas",
31
37
  "skills",
@@ -43,6 +49,10 @@
43
49
  "types": "./dist/config/index.d.ts",
44
50
  "default": "./dist/config/index.js"
45
51
  },
52
+ "./cli": {
53
+ "types": "./dist/cli/index.d.ts",
54
+ "default": "./dist/cli/index.js"
55
+ },
46
56
  "./client": {
47
57
  "types": "./dist/client/index.d.ts",
48
58
  "default": "./dist/client/index.js"
@@ -174,6 +184,7 @@
174
184
  "dependencies": {
175
185
  "@standard-schema/spec": "1.1.0",
176
186
  "@supabase/middleware": "1.0.0",
187
+ "@supabase/postgrest-js": "2.117.2",
177
188
  "@supabase/postgrest-typegen": "0.3.1",
178
189
  "@supabase/server": "1.9.0",
179
190
  "@supabase/ssr": "0.12.7"
@@ -182,12 +193,16 @@
182
193
  "@arethetypeswrong/cli": "0.18.5",
183
194
  "@better-supabase/ox-config": "0.0.0",
184
195
  "@better-supabase/typescript-config": "0.0.0",
196
+ "@clack/prompts": "1.8.1",
197
+ "@effect/platform-node": "4.0.0",
185
198
  "@opentelemetry/api": "1.9.1",
186
199
  "@orpc/server": "2.0.0-beta.40",
200
+ "@supabase/config": "0.11.1",
187
201
  "@supabase/supabase-js": "2.117.2",
202
+ "@t3-oss/env-core": "0.13.11",
188
203
  "@tanstack/query-core": "5.104.0",
189
204
  "@tanstack/react-query": "5.104.0",
190
- "@types/node": "24.19.0",
205
+ "@types/node": "26.6.3",
191
206
  "@types/pg": "8.23.1",
192
207
  "@types/react": "19.3.0",
193
208
  "@types/react-dom": "19.3.0",
@@ -195,14 +210,22 @@
195
210
  "@vitest/coverage-v8": "5.0.3",
196
211
  "ajv": "8.20.0",
197
212
  "ajv-formats": "3.0.1",
213
+ "c12": "4.0.0-rc.2",
214
+ "citty": "0.2.2",
215
+ "diff": "9.0.0",
216
+ "effect": "4.0.0",
217
+ "fastest-levenshtein": "1.0.16",
198
218
  "hono": "4.13.12",
199
219
  "next": "16.3.8",
220
+ "oxfmt": "0.71.0",
200
221
  "oxlint": "1.86.0",
201
222
  "pg": "8.23.1",
202
223
  "publint": "0.3.24",
203
224
  "react": "19.3.0",
204
225
  "react-dom": "19.3.0",
226
+ "smol-toml": "1.9.0",
205
227
  "temporal-polyfill": "1.0.5",
228
+ "tinyexec": "1.3.1",
206
229
  "tsdown": "0.23.0",
207
230
  "typescript": "7.0.2",
208
231
  "valibot": "1.5.0",
@@ -212,10 +235,12 @@
212
235
  "peerDependencies": {
213
236
  "@opentelemetry/api": ">=1.9",
214
237
  "@orpc/server": ">=2.0.0-0",
238
+ "@supabase/config": ">=0.9.0 <0.12",
215
239
  "@supabase/supabase-js": "^2.116.0",
216
240
  "@tanstack/query-core": "^5",
217
241
  "hono": ">=4",
218
242
  "next": ">=16",
243
+ "oxfmt": ">=0.66",
219
244
  "pg": ">=8",
220
245
  "react": "^19",
221
246
  "temporal-polyfill": ">=1"
@@ -227,6 +252,9 @@
227
252
  "@orpc/server": {
228
253
  "optional": true
229
254
  },
255
+ "@supabase/config": {
256
+ "optional": true
257
+ },
230
258
  "@tanstack/query-core": {
231
259
  "optional": true
232
260
  },
@@ -236,6 +264,9 @@
236
264
  "next": {
237
265
  "optional": true
238
266
  },
267
+ "oxfmt": {
268
+ "optional": true
269
+ },
239
270
  "pg": {
240
271
  "optional": true
241
272
  },
@@ -253,8 +284,9 @@
253
284
  "bench": "node tests/next/next.bench.ts",
254
285
  "build": "tsdown",
255
286
  "check:publish": "publint && pnpm pack --out pack.tgz && attw --profile esm-only pack.tgz && rm -f pack.tgz",
287
+ "gen:fixtures": "node scripts/gen-fixtures.ts",
256
288
  "lint": "oxlint",
257
- "test": "vitest run --coverage",
289
+ "test": "vitest run --project unit --coverage",
258
290
  "transit": "true",
259
291
  "typecheck": "tsc --noEmit"
260
292
  }
@@ -1,14 +1,18 @@
1
1
  ---
2
2
  name: better-supabase
3
- description: Query and write Supabase data with better-supabase's typed repositories, Results and codegen. Use when code imports better-supabase, when adding a table, query or mutation, or when a Supabase type is out of date.
3
+ description: Query and write Supabase data with better-supabase's typed repositories, Results and codegen. Use when code imports better-supabase, when adding a table, query or mutation, when a Supabase type is out of date, or after upgrading better-supabase.
4
4
  ---
5
5
 
6
6
  # better-supabase
7
7
 
8
- The data layer is `sb = defineSupabase(schema)` in `src/lib/supabase.ts`.
8
+ The data layer is `betterSupabase = defineSupabase(schema)` in `src/lib/supabase/index.ts`.
9
9
  `schema` comes from the generated module (`config.output`, usually
10
10
  `src/lib/supabase/generated.ts`). Never edit generated files.
11
11
 
12
+ The `better-supabase` command ships in the `better-supabase` package, so
13
+ `pnpm better-supabase <command>` works once the package is installed. There
14
+ is no separate CLI package to add.
15
+
12
16
  ## Workflow: change the schema
13
17
 
14
18
  1. Write the migration: edit `supabase/schemas/*.sql`, then run
@@ -18,15 +22,18 @@ The data layer is `sb = defineSupabase(schema)` in `src/lib/supabase.ts`.
18
22
  data and role settings.
19
23
  2. For a new table in an exposed schema, in the same file: `enable row level
20
24
  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
25
+ every column a policy filters by. Write tenant checks as
26
+ `organization_id in (select better_supabase.member_org_ids())`, which
27
+ Postgres runs once per statement, not `has_org_role(organization_id)`,
28
+ which runs once per row. List the table in `expose` in
22
29
  `better-supabase.config.ts` and run `pnpm better-supabase sql add grants`:
23
30
  new tables get no Data API grants on their own.
24
31
  3. Apply it with `supabase db reset` (or `supabase migration up`).
25
32
  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
33
  5. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
28
34
  missing policies, missing grants, unindexed foreign keys, drift).
29
- 6. Commit the generated files. CI runs `better-supabase gen --check`.
35
+ 6. Commit the generated files. CI runs `better-supabase gen --check`, which
36
+ prints a diff of each stale file.
30
37
 
31
38
  Done when `gen --check` and `doctor` exit 0 and the project typechecks.
32
39
 
@@ -44,14 +51,39 @@ Done when the call typechecks without casts, the error path returns the
44
51
  `DbError` (not a thrown exception), and a test shows another tenant's rows
45
52
  stay hidden.
46
53
 
54
+ ## Workflow: after upgrading better-supabase
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
60
+ composite foreign key such as `(customer_id, organization_id)` is named
61
+ after the remaining column (`customer`); set `tables.<name>.relations` in
62
+ the config to keep an old relation name.
63
+ 3. Run `pnpm better-supabase skills install --check` and reinstall the skills
64
+ when it reports them stale.
65
+ 4. Read the release's breaking changes in
66
+ https://github.com/ScaleDockHQ/better-supabase/blob/main/CHANGELOG.md
67
+ (guides live under https://bettersupabase.com/docs/migration). For 0.3: time values are
68
+ `Temporal` (see below), `--db-url` is gone, `doctor --format json` is
69
+ `doctor --json`, `BetterResultShape` is `BetterResultValue`, and an
70
+ exhaustive `switch` over the `invalid` auth reason needs an `'actor'` case.
71
+
72
+ Done when `gen --check`, `doctor` and the typecheck pass.
73
+
47
74
  ## Where `db` comes from
48
75
 
49
- - Next.js: `const { db } = await next.server()`, `next.route(...)`, `next.action(...)`
50
- - Next.js Cache Components: keep layouts synchronous; read `next.session()` in a `'use cache: private'` function inside `<Suspense>`, pass the promise to `<SessionProvider>` and read it with `useSession()`
76
+ Name the definition `betterSupabase` (in `lib/supabase/index.ts`) and every runtime instance `bs` (`lib/supabase/server.ts`, `lib/supabase/client.ts`, or the Hono, oRPC or Edge Function entry).
77
+
78
+ - Next.js: `const { db } = await bs.context()`, `bs.route(...)`, `bs.action(...)`
79
+ - Next.js Cache Components: keep layouts synchronous; read `bs.session()` in a `'use cache: private'` function inside `<Suspense>`, pass the promise to `<SessionProvider>` and read it with `useSession()`
51
80
  - Hono: `c.var.db` after `bs.middleware()`
52
81
  - oRPC: `context.db` after `bs.middleware()`
53
82
  - Edge Functions: `bs.handler((request, { db }) => ...)`
54
- - Browser: `browser.db`, or the hooks from `createHooks<typeof browser>()`
83
+ - Browser: `bs.db`, or the hooks from `createHooks<typeof bs>()`
84
+
85
+ For who the caller is (sessions, claims, OAuth clients, agents, scopes), use
86
+ the `better-supabase-auth` skill.
55
87
 
56
88
  ## Querying
57
89
 
@@ -68,18 +100,33 @@ const customers = result.data;
68
100
  ```
69
101
 
70
102
  - Methods return a `Result`, and database errors never throw. Use `.orThrow()` only where an exception is really wanted.
71
- - Column names use the configured casing (`casing: 'camel'` means `organizationId`). Raw escape hatches (`$client`, `$sql`) use database names.
103
+ - Column names use the configured casing (`casing: 'camel'` means `organizationId`). Raw escape hatches (`$client`, and `queryRaw` on the `better-supabase/postgres` clients) use database names.
72
104
  - Writes: `create`, `createMany`, `update(id, patch)`, `updateMany`, `upsert`, `delete`.
105
+ - Page lists with a cursor: `paginate({ after: null, size: 25, orderBy })`, then pass `nextCursor` back as `after` with the same `orderBy` and `where`. Use `page` numbers only when users jump to a page and need a total.
73
106
  - Handlers may return a `Result` directly. Adapters turn errors into RFC 9457 Problem Details with the right status.
74
- - Server-only admin access: `server.admin()`. Only use it for trusted jobs, never for a user's request.
107
+ - Server-only admin access: `bs.admin()`. Only use it for trusted jobs, never for a user's request.
75
108
  - PostgREST has no multi-request transactions. Put multi-step writes in a database function (`db.$rpc()`) or use `postgres.transaction()` on the server.
76
109
 
110
+ ## Time values
111
+
112
+ - With `codecs: { timestamptz: 'instant' }` in the config, `timestamptz` columns decode to `Temporal.Instant` and `timestamp` columns to `Temporal.PlainDateTime`, with microseconds. Without the codec they stay ISO strings.
113
+ - `now` options, job and webhook timestamps and `bucket.sweep({ olderThan })` take `Temporal` values. Auth's `now` stays in epoch milliseconds.
114
+ - On Node 24, Safari and other runtimes without `Temporal`, add `temporal-polyfill` and `import "temporal-polyfill/global"` once at startup. Without it, calls that need `Temporal` return an `unexpected` `DbError` that names the import.
115
+ - Use TypeScript 6 or 7; 5.9 has no Temporal lib.
116
+
117
+ ## CLI in scripts and CI
118
+
119
+ - No command takes a connection string as an argument. Set `$DATABASE_URL` or `source.dbUrl`, or pipe it in with `--db-url-stdin`.
120
+ - `--json` prints one JSON document on stdout: the result, or Problem Details with a `code` on failure. `--yes` and `CI` turn prompts off.
121
+ - Exit codes: 0 success, 1 a failure or finding (`--check` drift, doctor errors), 2 a usage, config or environment problem. Each error code is listed at https://bettersupabase.com/docs/cli/errors.
122
+
77
123
  ## Don't
78
124
 
79
125
  - Don't create supabase-js clients by hand, and don't read cookies yourself. Use the adapters.
80
126
  - Don't add `declare module` augmentation for types; everything comes from `schema`.
81
127
  - Don't catch and swallow `DbError`; return it, or map it with `mapDbError`.
82
128
  - Don't cast rows (`as Customer`). If a type is wrong, the schema or the generated file is out of date: rerun `gen`.
129
+ - Don't convert `Temporal` values to `Date` to compare them; use `.equals()` or `Temporal.Instant.compare`.
83
130
 
84
131
  ## When something fails
85
132
 
@@ -1,6 +1,6 @@
1
1
  # Plugins
2
2
 
3
- ```ts title="src/lib/supabase.ts"
3
+ ```ts title="src/lib/supabase/index.ts"
4
4
  import { defineSupabase } from "better-supabase";
5
5
  import { actor } from "better-supabase/plugins/actor";
6
6
  import { softDelete } from "better-supabase/plugins/soft-delete";
@@ -8,10 +8,10 @@ import { tenant } from "better-supabase/plugins/tenant";
8
8
  import { timestamps } from "better-supabase/plugins/timestamps";
9
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 "./generated.ts";
12
+ import { validators } from "./generated.zod.ts";
13
13
 
14
- export const sb = defineSupabase(schema)
14
+ export const betterSupabase = defineSupabase(schema)
15
15
  .use(timestamps())
16
16
  .use(softDelete())
17
17
  .use(tenant())
@@ -57,12 +57,8 @@ The tenant comes from the verified `tenant_id` claim (renamed with
57
57
  `claims.tenant` in `better-supabase.config.ts`), then `app_metadata.tenant_id`.
58
58
  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
- 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`.
60
+ memberships: don't run `sql add tenant`, and follow the PermDock reference of
61
+ the `better-supabase-auth` skill
62
+ (https://bettersupabase.com/docs/auth/permdock.md).
67
63
 
68
64
  Docs: https://bettersupabase.com/docs/plugins.md
@@ -11,7 +11,7 @@ Branch on `result.error.kind`, not on the message.
11
11
  | ----------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
12
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
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()` |
14
+ | `forbidden` | RLS or a grant rejected the write | Fix the policy or the grant; don't switch to `bs.admin()` |
15
15
  | `conflict` | Unique violation | Use `upsert`, or return the error to the caller |
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 |
@@ -22,18 +22,38 @@ Branch on `result.error.kind`, not on the message.
22
22
  | `stale` | `update(..., { expect })` found a newer row | Reload the row and retry |
23
23
  | `serialization`, `timeout`, `network`, `rate_limited` | Transient | Retry with backoff, or surface the error |
24
24
  | `raised` | A database function raised an exception | Read `message` and `code` from the function |
25
+ | `unexpected` naming `temporal-polyfill/global` | The runtime has no `Temporal` (Node 24, Safari) | Install `temporal-polyfill` and import `temporal-polyfill/global` at startup |
26
+
27
+ ## By error `code`
28
+
29
+ | Code | Meaning | Fix |
30
+ | -------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
31
+ | `CLAIMS_INVALID` (unauthorized) | The token verified but failed the `betterSupabase.claims(schema)` schema | Fix the access token hook or loosen the schema; the error names the failing paths |
32
+ | `SESSION_REVOKED` (unauthorized) | `checkSession` found the session gone (signed out, or ended by an admin) | Expected for irreversible actions; ask the user to sign in again |
33
+ | `INSUFFICIENT_SCOPE` (forbidden) | A delegated token (OAuth client or agent) lacks a scope the route requires | The 403 carries an `insufficient_scope` challenge; the client asks the user for that scope |
25
34
 
26
35
  ## Other symptoms
27
36
 
28
37
  - Types don't match the database: run `pnpm better-supabase gen`, then
29
38
  `gen --check` in CI.
30
39
  - A column name is `snake_case` in one place and `camelCase` in another:
31
- repositories use the configured casing, `$client` and `$sql` use database
32
- names.
40
+ repositories use the configured casing, `$client` and `queryRaw` use
41
+ database names.
33
42
  - Every request calls the Auth server: the token is being refreshed outside
34
- the proxy. Refresh happens only in the Next.js proxy (`next.proxy`).
43
+ the proxy. Refresh happens only in the Next.js proxy (`bs.proxy`).
35
44
  - Tests pass with the service role but fail as a user: that's the RLS policy.
36
45
  Test as users, never with the service role.
46
+ - A relation name changed after `gen`: a composite foreign key that repeats a
47
+ column on both sides is named after the remaining column. Set
48
+ `tables.<name>.relations` in the config to keep the old name.
49
+ - The CLI exits with 2: the command line, the config or an environment
50
+ variable needs a change. Rerun with `--json` to get the error `code`, and
51
+ look it up at https://bettersupabase.com/docs/cli/errors.
52
+ - `--db-url` is rejected: set `$DATABASE_URL` or pipe the URL in with
53
+ `--db-url-stdin`.
54
+ - Two different `Temporal.Instant` values compare equal in `toEqual`: compare
55
+ with `.equals()` or register an equality tester (the
56
+ `better-supabase-testing` skill).
37
57
 
38
58
  Docs: https://bettersupabase.com/docs/cli/doctor.md and
39
59
  https://bettersupabase.com/docs/guides/limitations.md
@@ -1,28 +1,32 @@
1
1
  ---
2
2
  name: better-supabase-api
3
- description: Build HTTP APIs, Edge Functions and MCP servers on Supabase with better-supabase adapters. Use when adding a route, REST resource, oRPC procedure, Edge Function, MCP tool, webhook, background job or idempotent endpoint.
3
+ description: Build HTTP APIs, Edge Functions and MCP servers on Supabase with better-supabase adapters. Use when adding a route, REST resource, oRPC procedure, Edge Function, MCP tool, OAuth or agent scopes, webhook, background job, cleanup cron or idempotent endpoint.
4
4
  ---
5
5
 
6
6
  # APIs with better-supabase
7
7
 
8
8
  Every adapter resolves the caller once and hands you `{ auth, db, supabase }`.
9
- `auth.kind` is `user`, `anon`, `service` or `invalid`.
9
+ `auth.kind` is `user`, `anon`, `service` or `invalid`. For sessions, claims
10
+ and the OAuth client or agent behind a token, see the `better-supabase-auth`
11
+ skill.
10
12
 
11
13
  ## Workflow: add an endpoint
12
14
 
13
15
  1. Pick the adapter for the runtime (table below). Setup code for each is in
14
16
  [references/adapters.md](references/adapters.md).
15
- 2. Decide who may call it with `allow` (see below).
17
+ 2. Decide who may call it with `allow`, and which scopes a delegated token
18
+ needs with `scopes` (see below).
16
19
  3. Use `db` from the handler context and return its `Result`, a plain value
17
20
  or a `Response`.
18
21
  4. For a table exposed as REST, prefer `bs.resource(...)` over hand-written
19
22
  routes, and regenerate `openapi.json` with `better-supabase openapi emit`.
20
- 5. Add an API test that calls the endpoint as a user and as another tenant
21
- (the `better-supabase-testing` skill).
23
+ 5. Add an API test that calls the endpoint as a user and as another tenant,
24
+ and with a delegated token that lacks the scope (the
25
+ `better-supabase-testing` skill).
22
26
 
23
- Done when the endpoint rejects callers outside `allow`, errors come back as
24
- Problem Details (no hand-built error JSON), and `openapi emit --check` passes
25
- if the project has an OpenAPI file.
27
+ Done when the endpoint rejects callers outside `allow` and tokens without its
28
+ `scopes`, errors come back as Problem Details (no hand-built error JSON), and
29
+ `openapi emit --check` passes if the project has an OpenAPI file.
26
30
 
27
31
  ## Who gets in
28
32
 
@@ -30,15 +34,36 @@ if the project has an OpenAPI file.
30
34
  and `['service']` for machine callers. Rejected callers get a 401 with a
31
35
  `WWW-Authenticate` header, or a 403, as Problem Details.
32
36
 
37
+ `scopes` limits what a delegated token may do: a token from the Supabase
38
+ OAuth server (`client_id` and `scope`) or one exchanged for an agent (an
39
+ RFC 8693 `act` chain). The guard options of `bs.route`, `bs.action`,
40
+ `bs.handler` (edge) and `bs.middleware` (Hono, oRPC) take it, next to
41
+ `allow`:
42
+
43
+ ```ts
44
+ export const GET = bs.route(
45
+ (request, { db }) => db.customers.findMany({ limit: 20 }),
46
+ { scopes: ["customers:read"] },
47
+ );
48
+ ```
49
+
50
+ With `.claims(schema)` on the definition, `auth.claims` is typed in every
51
+ adapter (`c.var.auth`, `context.auth`, the edge handler's `auth`, MCP's
52
+ `ctx.auth`). Read roles from it; don't parse the claims again.
53
+
54
+ A token without the scope gets a 403 with an `insufficient_scope`
55
+ challenge. The user's own session holds no `client_id` or `act`, so `scopes`
56
+ never limits it. RLS still decides the rows: the token's `sub` is the user.
57
+
33
58
  ## Adapters
34
59
 
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 }) => ... })` |
60
+ | Where | Setup | Handler |
61
+ | -------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
62
+ | Next.js | `createNext(betterSupabase)` in `lib/supabase/server.ts` | `bs.route((req, { db }) => ...)`, `bs.action({ input: schema }, (input, { db }) => ...)` |
63
+ | Hono | `createHono(betterSupabase)`, `.use('/api/*', bs.middleware())` | `c.var.db`; `bs.resource('customers', {...})` for REST |
64
+ | oRPC | `createOrpc(betterSupabase)`, `base.use(bs.middleware())` | `bs.unwrap(context.db.customers.findMany(...))` |
65
+ | Edge Functions | `createEdge(betterSupabase, { cors: true })` | `Deno.serve(bs.handler((req, { db }) => ...))` |
66
+ | MCP | `createMcp(betterSupabase, { name, version, resources })` | `.tool({ name, input, run: (args, { db }) => ... })` |
42
67
 
43
68
  Don't build error JSON by hand; errors become Problem Details.
44
69
 
@@ -46,17 +71,38 @@ Don't build error JSON by hand; errors become Problem Details.
46
71
 
47
72
  `defineResource` / `bs.resource(table, { operations, list, input })` gives you
48
73
  list, get, create, update and delete, with validation and paging (`{ items, page }`).
49
- `createOpenApi(sb, { resources })` describes the same routes, and
74
+ `createOpenApi(betterSupabase, { resources })` describes the same routes, and
50
75
  `better-supabase openapi emit --check` keeps `openapi.json` in sync.
51
76
 
77
+ Pass `pagination: "cursor"` to a resource, `defineListQuery` or an MCP table
78
+ for lists that clients read page by page: the list takes `after` instead of
79
+ `page`, returns `nextCursor` and `hasMore`, and `size` stays capped by
80
+ `maxPageSize`. The OpenAPI document and the JSON Schema describe the cursor.
81
+
82
+ ## MCP servers
83
+
84
+ - Tools run as the calling user, so RLS applies to every call.
85
+ - `authorize(ctx, tool, args)` refuses a call (`{ allowed: false, reason, scopes }`; with `scopes` it is a 403 `insufficient_scope`). `visible(ctx, tool)` hides tools from `tools/list`. Put a permission or a label in a tool's `meta`; clients never see it.
86
+ - `scopes` on `createMcp` only advertises scopes. Refuse calls in `authorize`, reading `toSession(ctx.auth).delegation?.scopes`.
87
+ - Set `allowedOrigins` and `allowedHosts` (every host the server answers on, previews and local included) against DNS rebinding, and `resourceDocumentation` to a page that explains how to connect.
88
+
52
89
  ## Background work (`better-supabase/jobs`, needs `sql add jobs idempotency webhook-inbox`)
53
90
 
54
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).
55
92
  - `createIdempotency(postgres.admin).handle(request, handler)` for POST endpoints that clients retry.
56
93
  - `createInbox(postgres.admin, { source, secrets }).receive(request)` for webhooks; `process(handler)` later.
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.
57
96
 
58
97
  Done when a failing job is retried and then archived after `maxAttempts`,
59
- and a replayed webhook or POST doesn't run twice.
98
+ a replayed webhook or POST doesn't run twice, and each kit table has a purge
99
+ schedule.
100
+
101
+ ## Direct Postgres on the server
102
+
103
+ - `createPostgres()` opens a pool; `createPostgres({ pool })` runs on an existing `pg.Pool`. `asUser` and `anon` transactions get Supabase's role timeouts (8 s and 3 s); `statementTimeout` takes a number or `{ admin, authenticated, anon }`.
104
+ - `createMany` and `upsertMany` split inserts past 65,535 bind parameters into several statements in one transaction, so large batches don't need manual chunking.
105
+ - In serverless functions, connect through the transaction pooler (port 6543), not a direct connection.
60
106
 
61
107
  ## Testing
62
108
 
@@ -64,4 +110,6 @@ Sign tokens with `signLocalJwt` or `asUser` from `better-supabase/testing`;
64
110
  the adapter verifies them against the local JWKS. See the `better-supabase-testing` skill.
65
111
 
66
112
  Docs: https://bettersupabase.com/docs/frameworks/hono.md (and `next`,
67
- `orpc`, `edge`, `mcp` under `/docs/frameworks/`).
113
+ `orpc`, `edge`, `mcp` under `/docs/frameworks/`),
114
+ https://bettersupabase.com/docs/kits/jobs.md and
115
+ https://bettersupabase.com/docs/auth/postgres.md.