better-supabase 0.2.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 (190) hide show
  1. package/README.md +37 -33
  2. package/dist/base64-D2gsioCY.js +42 -0
  3. package/dist/claims-BAf6c7is.js +48 -0
  4. package/dist/cli/_format-Cax5EsM9.js +101 -0
  5. package/dist/cli/bin.js +39 -6
  6. package/dist/cli/command-CK14fP91.js +502 -0
  7. package/dist/cli/compare-BmIWVEui.js +9 -0
  8. package/dist/cli/config-D9R0o1LH.js +1895 -0
  9. package/dist/cli/config-SSKSTUl2.js +2 -0
  10. package/dist/cli/db-DOBPt660.js +52 -0
  11. package/dist/cli/dist-B1MvLv-M.js +129 -0
  12. package/dist/cli/dist-CFgGknmZ.js +1112 -0
  13. package/dist/cli/dist-Du7VnEUG.js +621 -0
  14. package/dist/cli/doctor-BOym3mbL.js +2382 -0
  15. package/dist/cli/env-BZ0uy8XK.js +108 -0
  16. package/dist/cli/env-jIFUADLY.js +511 -0
  17. package/dist/cli/errors-4v4DTtda.js +56 -0
  18. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  19. package/dist/cli/gen-Ccn_p90L.js +2008 -0
  20. package/dist/cli/gen-D5cLCULx.js +2 -0
  21. package/dist/cli/index.d.ts +295 -28
  22. package/dist/cli/index.js +184 -3
  23. package/dist/cli/init-ClnxRYBF.js +550 -0
  24. package/dist/cli/introspect-DxqfkVIv.js +81 -0
  25. package/dist/cli/io-k41_z-jT.js +24 -0
  26. package/dist/cli/json5-ChRycquj.js +787 -0
  27. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  28. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  29. package/dist/cli/keys-ByKHnvP9.js +83 -0
  30. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  31. package/dist/cli/openapi-8WHjxiTJ.js +66 -0
  32. package/dist/cli/project-B_GeXyHh.js +70 -0
  33. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  34. package/dist/cli/prompts-BTYQGunc.js +2 -0
  35. package/dist/cli/read-sets-CMa29t9a.js +971 -0
  36. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  37. package/dist/cli/run-CP20LKk_.js +347 -0
  38. package/dist/cli/seed-5J6eu_Xg.js +98 -0
  39. package/dist/cli/skills-LJJIhLFR.js +137 -0
  40. package/dist/cli/sql-D4VbqmXi.js +199 -0
  41. package/dist/cli/style-C0NFdJRL.js +29 -0
  42. package/dist/cli/toml-DB-MNnFs.js +424 -0
  43. package/dist/{version-Cqq1m3UY.js → cli/version-5mW_8sY1.js} +1 -1
  44. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  45. package/dist/client/index.d.ts +2 -2
  46. package/dist/client/index.js +15 -25
  47. package/dist/{compiler-CZ9nIuqy.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
  48. package/dist/config/index.d.ts +4 -3
  49. package/dist/config/index.js +5 -3
  50. package/dist/config-BpxDfnbg.js +129 -0
  51. package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
  52. package/dist/{config-ClDXmVtY.js → config-CbvFmGD8.js} +14 -137
  53. package/dist/{define-BXMYD9AY.js → define-Bh0AqTik.js} +221 -120
  54. package/dist/{define-e3zI4_4g.d.ts → define-CRS2MRV3.d.ts} +36 -31
  55. package/dist/edge/index.d.ts +9 -13
  56. package/dist/edge/index.js +51 -25
  57. package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
  58. package/dist/env/index.js +203 -1
  59. package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
  60. package/dist/events/index.d.ts +1 -1
  61. package/dist/events/index.js +139 -1
  62. package/dist/{events-DOt3nSO-.d.ts → events-B21UOhvP.d.ts} +4 -4
  63. package/dist/{executor-CGdNQ27g.d.ts → executor-B5IoUxLE.d.ts} +2 -2
  64. package/dist/{executor-DeppvirK.js → executor-CVt6dD0B.js} +9 -4
  65. package/dist/{executor-BYXQ9AGn.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
  66. package/dist/hono/index.d.ts +17 -24
  67. package/dist/hono/index.js +11 -11
  68. package/dist/{index-DuP14VHo.d.ts → index-528Ugmyp.d.ts} +30 -10
  69. package/dist/{index-Bw1bwtC0.d.ts → index-BAy4tsAP.d.ts} +28 -15
  70. package/dist/{index-CBDufH1M.d.ts → index-BBdvo-5d.d.ts} +24 -26
  71. package/dist/{index-DbxFr7XG.d.ts → index-BhHEC04r.d.ts} +9 -4
  72. package/dist/index-Cf-D2jk1.d.ts +50 -0
  73. package/dist/{index-Ciu5Xlml.d.ts → index-D5IfUIDh.d.ts} +6 -5
  74. package/dist/{index-K-3WM2Hp.d.ts → index-DFmOpBGC.d.ts} +13 -13
  75. package/dist/{index-D4v7sncu.d.ts → index-RlLXd8dL.d.ts} +13 -13
  76. package/dist/index.d.ts +17 -17
  77. package/dist/index.js +23 -14
  78. package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
  79. package/dist/jobs/index.d.ts +33 -26
  80. package/dist/jobs/index.js +44 -22
  81. package/dist/{json-schema-BpYqkCKf.js → json-schema-DP6V7cbr.js} +8 -3
  82. package/dist/lint/index.js +1 -1
  83. package/dist/list/index.d.ts +2 -2
  84. package/dist/list/index.js +131 -96
  85. package/dist/{live-DJyZMD6w.d.ts → live-BHakSWWo.d.ts} +26 -16
  86. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  87. package/dist/lookup-BaP5bI27.js +18 -0
  88. package/dist/mcp/index.d.ts +34 -27
  89. package/dist/mcp/index.js +59 -25
  90. package/dist/{mfa-TVia554c.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
  91. package/dist/{mfa-CpLfJSde.js → mfa-byfjruR9.js} +190 -46
  92. package/dist/next/image/index.js +6 -3
  93. package/dist/next/index.d.ts +64 -48
  94. package/dist/next/index.js +63 -50
  95. package/dist/openapi/index.d.ts +1 -1
  96. package/dist/openapi/index.js +61 -38
  97. package/dist/orpc/index.d.ts +18 -21
  98. package/dist/orpc/index.js +12 -12
  99. package/dist/otel/index.d.ts +5 -5
  100. package/dist/otel/index.js +84 -63
  101. package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-C4KFzLc9.js} +12 -7
  102. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  103. package/dist/{plugin-C4TR9J0Z.d.ts → plugin-bNhvJwRh.d.ts} +5 -4
  104. package/dist/plugins/actor/index.d.ts +1 -1
  105. package/dist/plugins/actor/index.js +1 -1
  106. package/dist/plugins/rules/index.d.ts +2 -2
  107. package/dist/plugins/rules/index.js +46 -16
  108. package/dist/plugins/soft-delete/index.d.ts +3 -3
  109. package/dist/plugins/soft-delete/index.js +4 -4
  110. package/dist/plugins/tenant/index.d.ts +3 -3
  111. package/dist/plugins/tenant/index.js +10 -4
  112. package/dist/plugins/timestamps/index.d.ts +1 -1
  113. package/dist/plugins/timestamps/index.js +2 -2
  114. package/dist/plugins/validation/index.d.ts +2 -2
  115. package/dist/plugins/validation/index.js +3 -3
  116. package/dist/pool-DFwEQwmN.d.ts +78 -0
  117. package/dist/postgres/index.d.ts +10 -5
  118. package/dist/postgres/index.js +37 -16
  119. package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
  120. package/dist/{problem-BTkTtRV9.js → problem-BihjPR0t.js} +11 -5
  121. package/dist/{problem-DlgB0lYl.d.ts → problem-U8UEKccv.d.ts} +6 -4
  122. package/dist/query/index.d.ts +2 -2
  123. package/dist/query/index.js +2 -2
  124. package/dist/{query-ChvDbsDE.js → query-BMMSWbI7.js} +19 -19
  125. package/dist/react/index.d.ts +7 -7
  126. package/dist/react/index.js +2 -2
  127. package/dist/react/server.d.ts +6 -6
  128. package/dist/react/server.js +3 -3
  129. package/dist/react/session.d.ts +21 -2
  130. package/dist/react/session.js +19 -1
  131. package/dist/{react-C83BdBGt.js → react-CQHMaESR.js} +59 -55
  132. package/dist/{read-set-4lI3W_gZ.js → read-set-FWK8qQ58.js} +51 -16
  133. package/dist/realtime/index.d.ts +4 -3
  134. package/dist/realtime/index.js +19 -17
  135. package/dist/{index-CG69s15v.d.ts → refresh-BwYkyNOQ.d.ts} +7 -3
  136. package/dist/{resolve-CjHILk2w.d.ts → resolve-BAqQcwMW.d.ts} +17 -11
  137. package/dist/{resource-CgZa3YG_.d.ts → resource-BJcaIfxi.d.ts} +17 -8
  138. package/dist/{resource-BHEWmQJK.js → resource-My_y-Syh.js} +43 -21
  139. package/dist/{respond-Dne1ARSa.d.ts → respond-DgIbdrt8.d.ts} +39 -14
  140. package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
  141. package/dist/{result-CTkCZ6JA.d.ts → result-CqEsbgYs.d.ts} +16 -11
  142. package/dist/server/index.d.ts +38 -13
  143. package/dist/server/index.js +43 -14
  144. package/dist/{respond-DMcbQwfN.js → server-BA75B0mT.js} +151 -96
  145. package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
  146. package/dist/{simplify-bwJpLnaS.js → simplify-Cqvfr9dj.js} +76 -39
  147. package/dist/sql/index.d.ts +139 -0
  148. package/dist/sql/index.js +1983 -0
  149. package/dist/{sql-DTjA1LPj.js → sql-B34HU2DI.js} +74 -17
  150. package/dist/ssr/index.d.ts +4 -4
  151. package/dist/ssr/index.js +2 -2
  152. package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
  153. package/dist/{standard-CyhRzctb.d.ts → standard-BsaCYQJc.d.ts} +1 -1
  154. package/dist/{stats-C5W-rBfR.js → stats-C_YxA2r1.js} +40 -14
  155. package/dist/storage/index.d.ts +4 -3
  156. package/dist/storage/index.js +548 -1
  157. package/dist/{tables-rZxRxNPW.js → tables-Cb-3_VWc.js} +19 -5
  158. package/dist/temporal-BwshQk0W.js +27 -0
  159. package/dist/temporal-required-CBNHT6ni.js +19 -0
  160. package/dist/testing/index.d.ts +25 -24
  161. package/dist/testing/index.js +165 -66
  162. package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
  163. package/dist/version-Bmf7_FzM.js +4 -0
  164. package/dist/{view-D1s0wa0T.d.ts → view-B64o81wp.d.ts} +36 -6
  165. package/dist/{view-aOYizTeE.js → view-BrJI2LKp.js} +11 -2
  166. package/dist/webhooks/index.d.ts +19 -17
  167. package/dist/webhooks/index.js +1 -1
  168. package/dist/{webhooks-DMipjhSP.js → webhooks-Bh_Jf_-l.js} +45 -24
  169. package/package.json +50 -22
  170. package/schemas/config-v1.json +31 -1
  171. package/schemas/doctor-report-v1.json +17 -1
  172. package/schemas/snapshot-v2.json +35 -0
  173. package/skills/better-supabase/SKILL.md +77 -18
  174. package/skills/better-supabase/references/plugins.md +21 -22
  175. package/skills/better-supabase/references/troubleshooting.md +38 -18
  176. package/skills/better-supabase-api/SKILL.md +66 -18
  177. package/skills/better-supabase-api/references/adapters.md +64 -41
  178. package/skills/better-supabase-auth/SKILL.md +146 -0
  179. package/skills/better-supabase-auth/references/permdock.md +55 -0
  180. package/skills/better-supabase-testing/SKILL.md +68 -11
  181. package/dist/casing-da0uRTqt.js +0 -14
  182. package/dist/claims-CoTobNC-.js +0 -25
  183. package/dist/cli-DrSgDOQC.js +0 -6646
  184. package/dist/env-BEToab0I.js +0 -204
  185. package/dist/events-BuzTgn2D.js +0 -135
  186. package/dist/pool-R2m_DkXi.d.ts +0 -37
  187. package/dist/seed-CzecODcJ.js +0 -101
  188. package/dist/session-C3LxnQ0e.d.ts +0 -22
  189. package/dist/session-CqEwzVup.js +0 -20
  190. package/dist/storage-DuGn38fr.js +0 -527
@@ -1,23 +1,39 @@
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
- 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`.
18
+ 1. Write the migration: edit `supabase/schemas/*.sql`, then run
19
+ `supabase db schema declarative sync -f <name>` (pg-delta;
20
+ `supabase db diff -f <name>` when `config.toml` has no
21
+ `[experimental.pgdelta] enabled = true`), or `supabase migration new` for
22
+ data and role settings.
23
+ 2. For a new table in an exposed schema, in the same file: `enable row level
24
+ security`, a policy per role and command the app uses, and an index on
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
29
+ `better-supabase.config.ts` and run `pnpm better-supabase sql add grants`:
30
+ new tables get no Data API grants on their own.
31
+ 3. Apply it with `supabase db reset` (or `supabase migration up`).
32
+ 4. Run `pnpm better-supabase gen`, then fix the type errors it surfaces.
33
+ 5. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
34
+ missing policies, missing grants, unindexed foreign keys, drift).
35
+ 6. Commit the generated files. CI runs `better-supabase gen --check`, which
36
+ prints a diff of each stale file.
21
37
 
22
38
  Done when `gen --check` and `doctor` exit 0 and the project typechecks.
23
39
 
@@ -35,23 +51,48 @@ Done when the call typechecks without casts, the error path returns the
35
51
  `DbError` (not a thrown exception), and a test shows another tenant's rows
36
52
  stay hidden.
37
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
+
38
74
  ## Where `db` comes from
39
75
 
40
- - Next.js: `const { db } = await next.server()`, `next.route(...)`, `next.action(...)`
41
- - 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()`
42
80
  - Hono: `c.var.db` after `bs.middleware()`
43
81
  - oRPC: `context.db` after `bs.middleware()`
44
82
  - Edge Functions: `bs.handler((request, { db }) => ...)`
45
- - 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.
46
87
 
47
88
  ## Querying
48
89
 
49
90
  ```ts
50
91
  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' },
92
+ select: ["id", "name"],
93
+ where: { status: "active", notes: { some: { kind: "call" } } },
94
+ include: { organization: { select: ["name"] } },
95
+ orderBy: { name: "asc" },
55
96
  limit: 20,
56
97
  });
57
98
  if (!result.ok) return result; // DbError: kind, message, status, code
@@ -59,18 +100,33 @@ const customers = result.data;
59
100
  ```
60
101
 
61
102
  - Methods return a `Result`, and database errors never throw. Use `.orThrow()` only where an exception is really wanted.
62
- - 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.
63
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.
64
106
  - Handlers may return a `Result` directly. Adapters turn errors into RFC 9457 Problem Details with the right status.
65
- - 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.
66
108
  - PostgREST has no multi-request transactions. Put multi-step writes in a database function (`db.$rpc()`) or use `postgres.transaction()` on the server.
67
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
+
68
123
  ## Don't
69
124
 
70
125
  - Don't create supabase-js clients by hand, and don't read cookies yourself. Use the adapters.
71
126
  - Don't add `declare module` augmentation for types; everything comes from `schema`.
72
127
  - Don't catch and swallow `DbError`; return it, or map it with `mapDbError`.
73
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`.
74
130
 
75
131
  ## When something fails
76
132
 
@@ -82,3 +138,6 @@ See [references/troubleshooting.md](references/troubleshooting.md) for each
82
138
  https://bettersupabase.com/docs. Every page is also served as Markdown at
83
139
  `https://bettersupabase.com/docs/<path>.md`, and
84
140
  https://bettersupabase.com/llms.txt lists them all.
141
+
142
+ For Supabase itself (Auth, Storage, RLS, Postgres performance), install
143
+ Supabase's skills next to these: `npx skills add supabase/agent-skills`.
@@ -1,17 +1,17 @@
1
1
  # Plugins
2
2
 
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';
3
+ ```ts title="src/lib/supabase/index.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";
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())
@@ -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
 
@@ -57,9 +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, but PermDock's hook doesn't write it yet, so read entitlements from the
63
- database on the server instead of `hasEntitlement(session, ...)`.
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).
64
63
 
65
64
  Docs: https://bettersupabase.com/docs/plugins.md
@@ -7,33 +7,53 @@ 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 `bs.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
+ | `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.
@@ -1,58 +1,64 @@
1
1
  # Adapter setup
2
2
 
3
- Each adapter wraps the same `sb` from `src/lib/supabase.ts`:
3
+ Each adapter wraps the same `betterSupabase` from `src/lib/supabase/index.ts`:
4
4
 
5
- ```ts title="src/lib/supabase.ts"
6
- import { defineSupabase } from 'better-supabase';
5
+ ```ts title="src/lib/supabase/index.ts"
6
+ import { defineSupabase } from "better-supabase";
7
7
 
8
- import { schema } from './supabase/generated';
8
+ import { schema } from "./generated";
9
9
 
10
- export type { Functions, Models } from './supabase/generated';
10
+ export type { Functions, Models } from "./generated";
11
11
 
12
- export const sb = defineSupabase(schema);
12
+ export const betterSupabase = defineSupabase(schema);
13
13
  ```
14
14
 
15
15
  ## Next.js
16
16
 
17
- ```ts title="src/lib/supabase.server.ts"
18
- import { createNext } from 'better-supabase/next';
17
+ ```ts title="src/lib/supabase/server.ts"
18
+ import { createNext } from "better-supabase/next";
19
19
 
20
- import { sb } from './supabase';
20
+ import { betterSupabase } from "./index";
21
21
 
22
- export const next = createNext(sb);
22
+ export const bs = createNext(betterSupabase);
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 { bs } from "./lib/supabase/server";
29
29
 
30
- export const proxy = (request: NextRequest) => next.proxy(request);
30
+ export const proxy = (request: NextRequest) => bs.proxy(request);
31
31
  ```
32
32
 
33
33
  The proxy is the only place that refreshes sessions. Server Components,
34
34
  route handlers and actions read the verified token:
35
35
 
36
36
  ```ts
37
- const { db } = await next.server();
38
- export const GET = next.route((request, { db }) => db.customers.findMany({ limit: 20 }));
37
+ const { db } = await bs.context();
38
+ export const GET = bs.route(
39
+ (request, { db }) => db.customers.findMany({ limit: 20 }),
40
+ { scopes: ["customers:read"] }, // only limits OAuth clients and agents
41
+ );
39
42
  ```
40
43
 
41
44
  ## Hono
42
45
 
43
46
  ```ts title="src/server.ts"
44
- import { type BetterEnv, createHono } from 'better-supabase/hono';
45
- import { Hono } from 'hono';
47
+ import { type HonoEnv, createHono } from "better-supabase/hono";
48
+ import { Hono } from "hono";
46
49
 
47
- import { type Functions, type Models, sb } from './lib/supabase';
50
+ import { type Functions, type Models, betterSupabase } from "./lib/supabase";
48
51
 
49
- const bs = createHono(sb);
52
+ const bs = createHono(betterSupabase);
50
53
 
51
- const app = new Hono<BetterEnv<Models, Functions, unknown>>()
54
+ const app = new Hono<HonoEnv<Models, Functions, unknown>>()
52
55
  .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'] }));
56
+ .use("/api/*", bs.middleware())
57
+ .get("/api/me", (c) => c.json({ kind: c.var.auth.kind }))
58
+ .route(
59
+ "/api/customers",
60
+ bs.resource("customers", { select: ["id", "name"] }),
61
+ );
56
62
 
57
63
  export default app;
58
64
  ```
@@ -60,12 +66,12 @@ export default app;
60
66
  ## oRPC
61
67
 
62
68
  ```ts title="src/router.ts"
63
- import { os } from '@orpc/server';
64
- import { createOrpc, type OrpcRequestContext } from 'better-supabase/orpc';
69
+ import { os } from "@orpc/server";
70
+ import { createOrpc, type OrpcRequestContext } from "better-supabase/orpc";
65
71
 
66
- import { sb } from './lib/supabase';
72
+ import { betterSupabase } from "./lib/supabase";
67
73
 
68
- export const bs = createOrpc(sb);
74
+ export const bs = createOrpc(betterSupabase);
69
75
  const authed = os.$context<OrpcRequestContext>().use(bs.middleware());
70
76
 
71
77
  export const router = {
@@ -78,31 +84,48 @@ export const router = {
78
84
  ## Edge Functions
79
85
 
80
86
  ```ts title="supabase/functions/api/index.ts"
81
- import { createEdge } from 'better-supabase/edge';
87
+ import { createEdge } from "better-supabase/edge";
82
88
 
83
- import { sb } from '../_shared/supabase.ts';
89
+ import { betterSupabase } from "../_shared/supabase.ts";
84
90
 
85
- const bs = createEdge(sb, { cors: true });
91
+ const bs = createEdge(betterSupabase, { cors: true });
86
92
 
87
93
  Deno.serve(
88
- bs.resources({ customers: { select: ['id', 'name'] } }, { basePath: '/api' }),
94
+ bs.resources({ customers: { select: ["id", "name"] } }, { basePath: "/api" }),
89
95
  );
90
96
  ```
91
97
 
92
98
  ## MCP
93
99
 
94
100
  ```ts title="supabase/functions/mcp/index.ts"
95
- import { createMcp } from 'better-supabase/mcp';
96
-
97
- import { sb } from '../_shared/supabase.ts';
98
-
99
- const mcp = createMcp(sb, {
100
- name: 'crm',
101
- version: '0.1.0',
102
- resources: { customers: { select: ['id', 'name'] } },
101
+ import { createMcp } from "better-supabase/mcp";
102
+ import { toSession } from "better-supabase/server";
103
+
104
+ import { betterSupabase } from "../_shared/supabase.ts";
105
+
106
+ const bs = createMcp(betterSupabase, {
107
+ name: "crm",
108
+ version: "0.1.0",
109
+ scopes: ["openid", "crm.read"],
110
+ allowedOrigins: ["https://claude.ai"],
111
+ allowedHosts: ["crm.example.com", "localhost"],
112
+ resourceDocumentation: "https://crm.example.com/docs/mcp",
113
+ resources: {
114
+ customers: { select: ["id", "name"], pagination: "cursor" },
115
+ },
116
+ authorize: (ctx) => {
117
+ const session = toSession(ctx.auth);
118
+ const granted =
119
+ session.kind === "user" ? session.delegation?.scopes : undefined;
120
+ return !granted || granted.includes("crm.read")
121
+ ? { allowed: true }
122
+ : { allowed: false, reason: "Needs crm.read", scopes: ["crm.read"] };
123
+ },
103
124
  });
104
125
 
105
- Deno.serve(mcp.fetch);
126
+ Deno.serve(bs.fetch);
106
127
  ```
107
128
 
108
- Tools run as the calling user, so RLS applies to every tool call.
129
+ Tools run as the calling user, so RLS applies to every tool call. `scopes`
130
+ on `createMcp` only advertises scopes; `authorize` refuses the call. A
131
+ `delegation` that is unset means the user's own token, which no scope limits.