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.
- package/README.md +37 -33
- package/dist/base64-D2gsioCY.js +42 -0
- package/dist/claims-BAf6c7is.js +48 -0
- package/dist/cli/_format-Cax5EsM9.js +101 -0
- package/dist/cli/bin.js +39 -6
- package/dist/cli/command-CK14fP91.js +502 -0
- package/dist/cli/compare-BmIWVEui.js +9 -0
- package/dist/cli/config-D9R0o1LH.js +1895 -0
- package/dist/cli/config-SSKSTUl2.js +2 -0
- package/dist/cli/db-DOBPt660.js +52 -0
- package/dist/cli/dist-B1MvLv-M.js +129 -0
- package/dist/cli/dist-CFgGknmZ.js +1112 -0
- package/dist/cli/dist-Du7VnEUG.js +621 -0
- package/dist/cli/doctor-BOym3mbL.js +2382 -0
- package/dist/cli/env-BZ0uy8XK.js +108 -0
- package/dist/cli/env-jIFUADLY.js +511 -0
- package/dist/cli/errors-4v4DTtda.js +56 -0
- package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
- package/dist/cli/gen-Ccn_p90L.js +2008 -0
- package/dist/cli/gen-D5cLCULx.js +2 -0
- package/dist/cli/index.d.ts +295 -28
- package/dist/cli/index.js +184 -3
- package/dist/cli/init-ClnxRYBF.js +550 -0
- package/dist/cli/introspect-DxqfkVIv.js +81 -0
- package/dist/cli/io-k41_z-jT.js +24 -0
- package/dist/cli/json5-ChRycquj.js +787 -0
- package/dist/cli/jsonc-BUniUpOk.js +2 -0
- package/dist/cli/jsonc-D5Fu67jw.js +45 -0
- package/dist/cli/keys-ByKHnvP9.js +83 -0
- package/dist/cli/ohash-D7bG_EOb.js +277 -0
- package/dist/cli/openapi-8WHjxiTJ.js +66 -0
- package/dist/cli/project-B_GeXyHh.js +70 -0
- package/dist/cli/prompts-BNcNfAve.js +1142 -0
- package/dist/cli/prompts-BTYQGunc.js +2 -0
- package/dist/cli/read-sets-CMa29t9a.js +971 -0
- package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
- package/dist/cli/run-CP20LKk_.js +347 -0
- package/dist/cli/seed-5J6eu_Xg.js +98 -0
- package/dist/cli/skills-LJJIhLFR.js +137 -0
- package/dist/cli/sql-D4VbqmXi.js +199 -0
- package/dist/cli/style-C0NFdJRL.js +29 -0
- package/dist/cli/toml-DB-MNnFs.js +424 -0
- package/dist/{version-Cqq1m3UY.js → cli/version-5mW_8sY1.js} +1 -1
- package/dist/cli/yaml-CYCbJ6et.js +2252 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +15 -25
- package/dist/{compiler-CZ9nIuqy.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -3
- package/dist/config/index.js +5 -3
- package/dist/config-BpxDfnbg.js +129 -0
- package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
- package/dist/{config-ClDXmVtY.js → config-CbvFmGD8.js} +14 -137
- package/dist/{define-BXMYD9AY.js → define-Bh0AqTik.js} +221 -120
- package/dist/{define-e3zI4_4g.d.ts → define-CRS2MRV3.d.ts} +36 -31
- package/dist/edge/index.d.ts +9 -13
- package/dist/edge/index.js +51 -25
- package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
- package/dist/env/index.js +203 -1
- package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +139 -1
- package/dist/{events-DOt3nSO-.d.ts → events-B21UOhvP.d.ts} +4 -4
- package/dist/{executor-CGdNQ27g.d.ts → executor-B5IoUxLE.d.ts} +2 -2
- package/dist/{executor-DeppvirK.js → executor-CVt6dD0B.js} +9 -4
- package/dist/{executor-BYXQ9AGn.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
- package/dist/hono/index.d.ts +17 -24
- package/dist/hono/index.js +11 -11
- package/dist/{index-DuP14VHo.d.ts → index-528Ugmyp.d.ts} +30 -10
- package/dist/{index-Bw1bwtC0.d.ts → index-BAy4tsAP.d.ts} +28 -15
- package/dist/{index-CBDufH1M.d.ts → index-BBdvo-5d.d.ts} +24 -26
- package/dist/{index-DbxFr7XG.d.ts → index-BhHEC04r.d.ts} +9 -4
- package/dist/index-Cf-D2jk1.d.ts +50 -0
- package/dist/{index-Ciu5Xlml.d.ts → index-D5IfUIDh.d.ts} +6 -5
- package/dist/{index-K-3WM2Hp.d.ts → index-DFmOpBGC.d.ts} +13 -13
- package/dist/{index-D4v7sncu.d.ts → index-RlLXd8dL.d.ts} +13 -13
- package/dist/index.d.ts +17 -17
- package/dist/index.js +23 -14
- package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
- package/dist/jobs/index.d.ts +33 -26
- package/dist/jobs/index.js +44 -22
- package/dist/{json-schema-BpYqkCKf.js → json-schema-DP6V7cbr.js} +8 -3
- package/dist/lint/index.js +1 -1
- package/dist/list/index.d.ts +2 -2
- package/dist/list/index.js +131 -96
- package/dist/{live-DJyZMD6w.d.ts → live-BHakSWWo.d.ts} +26 -16
- package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
- package/dist/lookup-BaP5bI27.js +18 -0
- package/dist/mcp/index.d.ts +34 -27
- package/dist/mcp/index.js +59 -25
- package/dist/{mfa-TVia554c.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
- package/dist/{mfa-CpLfJSde.js → mfa-byfjruR9.js} +190 -46
- package/dist/next/image/index.js +6 -3
- package/dist/next/index.d.ts +64 -48
- package/dist/next/index.js +63 -50
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +61 -38
- package/dist/orpc/index.d.ts +18 -21
- package/dist/orpc/index.js +12 -12
- package/dist/otel/index.d.ts +5 -5
- package/dist/otel/index.js +84 -63
- package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-C4KFzLc9.js} +12 -7
- package/dist/permdock-sql-DImJULY-.d.ts +17 -0
- package/dist/{plugin-C4TR9J0Z.d.ts → plugin-bNhvJwRh.d.ts} +5 -4
- package/dist/plugins/actor/index.d.ts +1 -1
- package/dist/plugins/actor/index.js +1 -1
- package/dist/plugins/rules/index.d.ts +2 -2
- package/dist/plugins/rules/index.js +46 -16
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +4 -4
- package/dist/plugins/tenant/index.d.ts +3 -3
- package/dist/plugins/tenant/index.js +10 -4
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +2 -2
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/plugins/validation/index.js +3 -3
- package/dist/pool-DFwEQwmN.d.ts +78 -0
- package/dist/postgres/index.d.ts +10 -5
- package/dist/postgres/index.js +37 -16
- package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
- package/dist/{problem-BTkTtRV9.js → problem-BihjPR0t.js} +11 -5
- package/dist/{problem-DlgB0lYl.d.ts → problem-U8UEKccv.d.ts} +6 -4
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +2 -2
- package/dist/{query-ChvDbsDE.js → query-BMMSWbI7.js} +19 -19
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +2 -2
- package/dist/react/server.d.ts +6 -6
- package/dist/react/server.js +3 -3
- package/dist/react/session.d.ts +21 -2
- package/dist/react/session.js +19 -1
- package/dist/{react-C83BdBGt.js → react-CQHMaESR.js} +59 -55
- package/dist/{read-set-4lI3W_gZ.js → read-set-FWK8qQ58.js} +51 -16
- package/dist/realtime/index.d.ts +4 -3
- package/dist/realtime/index.js +19 -17
- package/dist/{index-CG69s15v.d.ts → refresh-BwYkyNOQ.d.ts} +7 -3
- package/dist/{resolve-CjHILk2w.d.ts → resolve-BAqQcwMW.d.ts} +17 -11
- package/dist/{resource-CgZa3YG_.d.ts → resource-BJcaIfxi.d.ts} +17 -8
- package/dist/{resource-BHEWmQJK.js → resource-My_y-Syh.js} +43 -21
- package/dist/{respond-Dne1ARSa.d.ts → respond-DgIbdrt8.d.ts} +39 -14
- package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
- package/dist/{result-CTkCZ6JA.d.ts → result-CqEsbgYs.d.ts} +16 -11
- package/dist/server/index.d.ts +38 -13
- package/dist/server/index.js +43 -14
- package/dist/{respond-DMcbQwfN.js → server-BA75B0mT.js} +151 -96
- package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
- package/dist/{simplify-bwJpLnaS.js → simplify-Cqvfr9dj.js} +76 -39
- package/dist/sql/index.d.ts +139 -0
- package/dist/sql/index.js +1983 -0
- package/dist/{sql-DTjA1LPj.js → sql-B34HU2DI.js} +74 -17
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +2 -2
- package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
- package/dist/{standard-CyhRzctb.d.ts → standard-BsaCYQJc.d.ts} +1 -1
- package/dist/{stats-C5W-rBfR.js → stats-C_YxA2r1.js} +40 -14
- package/dist/storage/index.d.ts +4 -3
- package/dist/storage/index.js +548 -1
- package/dist/{tables-rZxRxNPW.js → tables-Cb-3_VWc.js} +19 -5
- package/dist/temporal-BwshQk0W.js +27 -0
- package/dist/temporal-required-CBNHT6ni.js +19 -0
- package/dist/testing/index.d.ts +25 -24
- package/dist/testing/index.js +165 -66
- package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
- package/dist/version-Bmf7_FzM.js +4 -0
- package/dist/{view-D1s0wa0T.d.ts → view-B64o81wp.d.ts} +36 -6
- package/dist/{view-aOYizTeE.js → view-BrJI2LKp.js} +11 -2
- package/dist/webhooks/index.d.ts +19 -17
- package/dist/webhooks/index.js +1 -1
- package/dist/{webhooks-DMipjhSP.js → webhooks-Bh_Jf_-l.js} +45 -24
- package/package.json +50 -22
- package/schemas/config-v1.json +31 -1
- package/schemas/doctor-report-v1.json +17 -1
- package/schemas/snapshot-v2.json +35 -0
- package/skills/better-supabase/SKILL.md +77 -18
- package/skills/better-supabase/references/plugins.md +21 -22
- package/skills/better-supabase/references/troubleshooting.md +38 -18
- package/skills/better-supabase-api/SKILL.md +66 -18
- package/skills/better-supabase-api/references/adapters.md +64 -41
- package/skills/better-supabase-auth/SKILL.md +146 -0
- package/skills/better-supabase-auth/references/permdock.md +55 -0
- package/skills/better-supabase-testing/SKILL.md +68 -11
- package/dist/casing-da0uRTqt.js +0 -14
- package/dist/claims-CoTobNC-.js +0 -25
- package/dist/cli-DrSgDOQC.js +0 -6646
- package/dist/env-BEToab0I.js +0 -204
- package/dist/events-BuzTgn2D.js +0 -135
- package/dist/pool-R2m_DkXi.d.ts +0 -37
- package/dist/seed-CzecODcJ.js +0 -101
- package/dist/session-C3LxnQ0e.d.ts +0 -22
- package/dist/session-CqEwzVup.js +0 -20
- 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,
|
|
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 `
|
|
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
|
|
15
|
-
`supabase
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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: `
|
|
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: [
|
|
52
|
-
where: { status:
|
|
53
|
-
include: { organization: { select: [
|
|
54
|
-
orderBy: { name:
|
|
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`,
|
|
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: `
|
|
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
|
|
5
|
-
import { actor } from
|
|
6
|
-
import { softDelete } from
|
|
7
|
-
import { tenant } from
|
|
8
|
-
import { timestamps } from
|
|
9
|
-
import { validation } from
|
|
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
|
|
12
|
-
import { validators } from
|
|
11
|
+
import { schema } from "./generated.ts";
|
|
12
|
+
import { validators } from "./generated.zod.ts";
|
|
13
13
|
|
|
14
|
-
export const
|
|
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
|
|
38
|
-
|
|
|
39
|
-
| `timestamps()` | Sets `createdAt` and `updatedAt`
|
|
40
|
-
| `softDelete()` | Hides deleted rows, turns `delete` into an update, adds `restore`
|
|
41
|
-
| `tenant()`
|
|
42
|
-
| `actor()`
|
|
43
|
-
| `validation()` | Validates writes with any Standard Schema
|
|
44
|
-
| `rules()`
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
11
|
-
|
|
|
12
|
-
| `not_found`
|
|
13
|
-
| `unauthorized`
|
|
14
|
-
| `forbidden`
|
|
15
|
-
| `conflict`
|
|
16
|
-
| `foreign_key`
|
|
17
|
-
| `not_null`, `check`, `exclusion`
|
|
18
|
-
| `invalid_input`
|
|
19
|
-
| `validation`
|
|
20
|
-
| `invalid_request`
|
|
21
|
-
| `multiple_rows`
|
|
22
|
-
| `stale`
|
|
23
|
-
| `serialization`, `timeout`, `network`, `rate_limited` | Transient
|
|
24
|
-
| `raised`
|
|
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
|
|
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 (`
|
|
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
|
|
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
|
-
|
|
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
|
|
24
|
-
Problem Details (no hand-built error JSON), and
|
|
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
|
|
36
|
-
|
|
|
37
|
-
| Next.js
|
|
38
|
-
| Hono
|
|
39
|
-
| oRPC
|
|
40
|
-
| Edge Functions | `createEdge(
|
|
41
|
-
| MCP
|
|
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(
|
|
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
|
-
|
|
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 `
|
|
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
|
|
5
|
+
```ts title="src/lib/supabase/index.ts"
|
|
6
|
+
import { defineSupabase } from "better-supabase";
|
|
7
7
|
|
|
8
|
-
import { schema } from
|
|
8
|
+
import { schema } from "./generated";
|
|
9
9
|
|
|
10
|
-
export type { Functions, Models } from
|
|
10
|
+
export type { Functions, Models } from "./generated";
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const betterSupabase = defineSupabase(schema);
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Next.js
|
|
16
16
|
|
|
17
|
-
```ts title="src/lib/supabase
|
|
18
|
-
import { createNext } from
|
|
17
|
+
```ts title="src/lib/supabase/server.ts"
|
|
18
|
+
import { createNext } from "better-supabase/next";
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { betterSupabase } from "./index";
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const bs = createNext(betterSupabase);
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```ts title="src/proxy.ts"
|
|
26
|
-
import type { NextRequest } from
|
|
26
|
+
import type { NextRequest } from "next/server";
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import { bs } from "./lib/supabase/server";
|
|
29
29
|
|
|
30
|
-
export const proxy = (request: NextRequest) =>
|
|
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
|
|
38
|
-
export const GET =
|
|
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
|
|
45
|
-
import { Hono } from
|
|
47
|
+
import { type HonoEnv, createHono } from "better-supabase/hono";
|
|
48
|
+
import { Hono } from "hono";
|
|
46
49
|
|
|
47
|
-
import { type Functions, type Models,
|
|
50
|
+
import { type Functions, type Models, betterSupabase } from "./lib/supabase";
|
|
48
51
|
|
|
49
|
-
const bs = createHono(
|
|
52
|
+
const bs = createHono(betterSupabase);
|
|
50
53
|
|
|
51
|
-
const app = new Hono<
|
|
54
|
+
const app = new Hono<HonoEnv<Models, Functions, unknown>>()
|
|
52
55
|
.onError(bs.onError)
|
|
53
|
-
.use(
|
|
54
|
-
.get(
|
|
55
|
-
.route(
|
|
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
|
|
64
|
-
import { createOrpc, type OrpcRequestContext } from
|
|
69
|
+
import { os } from "@orpc/server";
|
|
70
|
+
import { createOrpc, type OrpcRequestContext } from "better-supabase/orpc";
|
|
65
71
|
|
|
66
|
-
import {
|
|
72
|
+
import { betterSupabase } from "./lib/supabase";
|
|
67
73
|
|
|
68
|
-
export const bs = createOrpc(
|
|
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
|
|
87
|
+
import { createEdge } from "better-supabase/edge";
|
|
82
88
|
|
|
83
|
-
import {
|
|
89
|
+
import { betterSupabase } from "../_shared/supabase.ts";
|
|
84
90
|
|
|
85
|
-
const bs = createEdge(
|
|
91
|
+
const bs = createEdge(betterSupabase, { cors: true });
|
|
86
92
|
|
|
87
93
|
Deno.serve(
|
|
88
|
-
bs.resources({ customers: { select: [
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
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.
|