better-supabase 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +88 -1
- package/bin/better-supabase.js +3 -0
- package/dist/casing-da0uRTqt.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +15 -0
- package/dist/cli/index.d.ts +226 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli-CoByWYja.js +6472 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +112 -0
- package/dist/compiler-DL18ukPe.d.ts +14 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +3 -0
- package/dist/config-D-aR2ZoN.js +276 -0
- package/dist/define-CTfWw-SS.d.ts +662 -0
- package/dist/define-QGkq04oe.js +1068 -0
- package/dist/edge/index.d.ts +43 -0
- package/dist/edge/index.js +103 -0
- package/dist/entitlements-BBg61DQZ.d.ts +20 -0
- package/dist/entitlements-BC_khwRy.js +17 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +2 -0
- package/dist/env-BEToab0I.js +204 -0
- package/dist/errors-Bl3rkOzo.js +196 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.js +2 -0
- package/dist/events-2OJsVG_d.d.ts +84 -0
- package/dist/events-BuzTgn2D.js +135 -0
- package/dist/executor-Bazhgsgq.d.ts +195 -0
- package/dist/executor-Bje4suQF.d.ts +25 -0
- package/dist/executor-Ceq60hPM.js +89 -0
- package/dist/hono/index.d.ts +45 -0
- package/dist/hono/index.js +70 -0
- package/dist/index-BD6wKthp.d.ts +597 -0
- package/dist/index-DQVvHzjW.d.ts +74 -0
- package/dist/index-DRid0qIv.d.ts +76 -0
- package/dist/index-Dup-nSe9.d.ts +117 -0
- package/dist/index-Dy6kuF0P.d.ts +71 -0
- package/dist/index-Hf8PP9sP.d.ts +116 -0
- package/dist/index-JXHBuS53.d.ts +144 -0
- package/dist/index-Nxo3TJcm.d.ts +208 -0
- package/dist/index-Sw43J8HG.d.ts +62 -0
- package/dist/index-cEeweTeM.d.ts +38 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +42 -0
- package/dist/invalidate-DbxuL2jL.js +18 -0
- package/dist/jobs/index.d.ts +173 -0
- package/dist/jobs/index.js +477 -0
- package/dist/json-schema-BbwO2mZg.js +181 -0
- package/dist/lint/index.d.ts +75 -0
- package/dist/lint/index.js +167 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/list/index.js +410 -0
- package/dist/live-6b9i72Ux.d.ts +180 -0
- package/dist/live-D8rV1Ldw.js +168 -0
- package/dist/mcp/index.d.ts +105 -0
- package/dist/mcp/index.js +330 -0
- package/dist/mfa-CpogP66z.js +521 -0
- package/dist/mfa-TVia554c.d.ts +44 -0
- package/dist/next/image/index.d.ts +34 -0
- package/dist/next/image/index.js +40 -0
- package/dist/next/index.d.ts +210 -0
- package/dist/next/index.js +426 -0
- package/dist/openapi/index.d.ts +2 -0
- package/dist/openapi/index.js +332 -0
- package/dist/orpc/index.d.ts +46 -0
- package/dist/orpc/index.js +50 -0
- package/dist/otel/index.d.ts +40 -0
- package/dist/otel/index.js +168 -0
- package/dist/path-Cv__-OTj.d.ts +17 -0
- package/dist/plugin-BqR6wKMB.js +23 -0
- package/dist/plugin-HvGqCurB.d.ts +118 -0
- package/dist/plugins/actor/index.d.ts +13 -0
- package/dist/plugins/actor/index.js +43 -0
- package/dist/plugins/rules/index.d.ts +88 -0
- package/dist/plugins/rules/index.js +174 -0
- package/dist/plugins/soft-delete/index.d.ts +38 -0
- package/dist/plugins/soft-delete/index.js +69 -0
- package/dist/plugins/tenant/index.d.ts +43 -0
- package/dist/plugins/tenant/index.js +73 -0
- package/dist/plugins/timestamps/index.d.ts +9 -0
- package/dist/plugins/timestamps/index.js +37 -0
- package/dist/plugins/validation/index.d.ts +24 -0
- package/dist/plugins/validation/index.js +59 -0
- package/dist/pool-BFeqzwQS.d.ts +37 -0
- package/dist/postgres/index.d.ts +28 -0
- package/dist/postgres/index.js +79 -0
- package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
- package/dist/problem-DPu6Fh-t.js +97 -0
- package/dist/problem-DlgB0lYl.d.ts +47 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +3 -0
- package/dist/query-ChvDbsDE.js +186 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +5 -0
- package/dist/react/server.d.ts +13 -0
- package/dist/react/server.js +25 -0
- package/dist/react/session.d.ts +2 -0
- package/dist/react/session.js +3 -0
- package/dist/react-CrOvrnIP.js +241 -0
- package/dist/read-set-DhX2d04c.js +216 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.js +253 -0
- package/dist/resolve-DW8h9NPm.d.ts +155 -0
- package/dist/resource-CYiu6Ygi.js +175 -0
- package/dist/resource-SkPTRJDD.d.ts +65 -0
- package/dist/respond-4-dhoV7x.js +380 -0
- package/dist/respond-PoTpy5DT.d.ts +169 -0
- package/dist/result-CTkCZ6JA.d.ts +198 -0
- package/dist/result-DWXatkd6.js +103 -0
- package/dist/scope-CDHcS7dE.js +90 -0
- package/dist/seed-CzecODcJ.js +101 -0
- package/dist/server/index.d.ts +42 -0
- package/dist/server/index.js +68 -0
- package/dist/session-C3osL21k.d.ts +21 -0
- package/dist/session-CD-3orO_.js +19 -0
- package/dist/shared-_6-uPL7E.js +57 -0
- package/dist/simplify-oHiDKRVQ.js +665 -0
- package/dist/spec-pins-CJPVghHy.d.ts +20 -0
- package/dist/spec-pins-t6sPaHvl.js +20 -0
- package/dist/sql-M8l3fnS0.js +260 -0
- package/dist/ssr/index.d.ts +5 -0
- package/dist/ssr/index.js +4 -0
- package/dist/standard-CyhRzctb.d.ts +7 -0
- package/dist/standard-FUi3Z3-7.js +21 -0
- package/dist/stats-3iy8u3_P.js +566 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage-BWXVUXGk.js +461 -0
- package/dist/tables-rZxRxNPW.js +59 -0
- package/dist/template-BeOGnynM.js +93 -0
- package/dist/template-mbvlysOo.d.ts +6 -0
- package/dist/testing/index.d.ts +294 -0
- package/dist/testing/index.js +751 -0
- package/dist/types-6cYK9pbJ.js +29 -0
- package/dist/types-C2fslP1z.d.ts +195 -0
- package/dist/version-Cqq1m3UY.js +4 -0
- package/dist/view-B0ltUmTi.d.ts +43 -0
- package/dist/view-B6F9QbG-.js +35 -0
- package/dist/webhooks/index.d.ts +152 -0
- package/dist/webhooks/index.js +2 -0
- package/dist/webhooks-DbMTJJX2.js +139 -0
- package/package.json +246 -17
- package/schemas/config-v1.json +328 -0
- package/schemas/doctor-report-v1.json +105 -0
- package/schemas/snapshot-v2.json +1002 -0
- package/skills/better-supabase/SKILL.md +84 -0
- package/skills/better-supabase/references/plugins.md +56 -0
- package/skills/better-supabase/references/troubleshooting.md +39 -0
- package/skills/better-supabase-api/SKILL.md +67 -0
- package/skills/better-supabase-api/references/adapters.md +108 -0
- package/skills/better-supabase-testing/SKILL.md +65 -0
- package/index.js +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# better-supabase
|
|
7
|
+
|
|
8
|
+
The data layer is `sb = defineSupabase(schema)` in `src/lib/supabase.ts`.
|
|
9
|
+
`schema` comes from the generated module (`config.output`, usually
|
|
10
|
+
`src/lib/supabase/generated.ts`). Never edit generated files.
|
|
11
|
+
|
|
12
|
+
## Workflow: change the schema
|
|
13
|
+
|
|
14
|
+
1. Write the migration (`supabase/schemas/*.sql` plus `supabase db diff`, or
|
|
15
|
+
`supabase migration new`).
|
|
16
|
+
2. Apply it with `supabase db reset` (or `supabase migration up`).
|
|
17
|
+
3. Run `pnpm better-supabase gen`, then fix the type errors it surfaces.
|
|
18
|
+
4. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
|
|
19
|
+
missing policies, unindexed foreign keys, drift).
|
|
20
|
+
5. Commit the generated files. CI runs `better-supabase gen --check`.
|
|
21
|
+
|
|
22
|
+
Done when `gen --check` and `doctor` exit 0 and the project typechecks.
|
|
23
|
+
|
|
24
|
+
## Workflow: add a query or mutation
|
|
25
|
+
|
|
26
|
+
1. Get `db` from the request context (see below), never from a global client,
|
|
27
|
+
so RLS applies.
|
|
28
|
+
2. Call the repository method and return or branch on its `Result`.
|
|
29
|
+
3. If a plugin owns a column (timestamps, soft delete, tenant, actor), leave
|
|
30
|
+
it out of the input. See [references/plugins.md](references/plugins.md).
|
|
31
|
+
4. Cover the rule with an RLS test as a real user (the
|
|
32
|
+
`better-supabase-testing` skill).
|
|
33
|
+
|
|
34
|
+
Done when the call typechecks without casts, the error path returns the
|
|
35
|
+
`DbError` (not a thrown exception), and a test shows another tenant's rows
|
|
36
|
+
stay hidden.
|
|
37
|
+
|
|
38
|
+
## Where `db` comes from
|
|
39
|
+
|
|
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()`
|
|
42
|
+
- Hono: `c.var.db` after `bs.middleware()`
|
|
43
|
+
- oRPC: `context.db` after `bs.middleware()`
|
|
44
|
+
- Edge Functions: `bs.handler((request, { db }) => ...)`
|
|
45
|
+
- Browser: `browser.db`, or the hooks from `createHooks<typeof browser>()`
|
|
46
|
+
|
|
47
|
+
## Querying
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
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' },
|
|
55
|
+
limit: 20,
|
|
56
|
+
});
|
|
57
|
+
if (!result.ok) return result; // DbError: kind, message, status, code
|
|
58
|
+
const customers = result.data;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- 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.
|
|
63
|
+
- Writes: `create`, `createMany`, `update(id, patch)`, `updateMany`, `upsert`, `delete`.
|
|
64
|
+
- 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.
|
|
66
|
+
- PostgREST has no multi-request transactions. Put multi-step writes in a database function (`db.$rpc()`) or use `postgres.transaction()` on the server.
|
|
67
|
+
|
|
68
|
+
## Don't
|
|
69
|
+
|
|
70
|
+
- Don't create supabase-js clients by hand, and don't read cookies yourself. Use the adapters.
|
|
71
|
+
- Don't add `declare module` augmentation for types; everything comes from `schema`.
|
|
72
|
+
- Don't catch and swallow `DbError`; return it, or map it with `mapDbError`.
|
|
73
|
+
- Don't cast rows (`as Customer`). If a type is wrong, the schema or the generated file is out of date: rerun `gen`.
|
|
74
|
+
|
|
75
|
+
## When something fails
|
|
76
|
+
|
|
77
|
+
See [references/troubleshooting.md](references/troubleshooting.md) for each
|
|
78
|
+
`DbError` kind and the usual cause.
|
|
79
|
+
|
|
80
|
+
## Docs
|
|
81
|
+
|
|
82
|
+
https://bettersupabase.com/docs. Every page is also served as Markdown at
|
|
83
|
+
`https://bettersupabase.com/docs/<path>.md`, and
|
|
84
|
+
https://bettersupabase.com/llms.txt lists them all.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Plugins
|
|
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';
|
|
10
|
+
|
|
11
|
+
import { schema } from './supabase/generated.ts';
|
|
12
|
+
import { validators } from './supabase/generated.zod.ts';
|
|
13
|
+
|
|
14
|
+
export const sb = defineSupabase(schema)
|
|
15
|
+
.use(timestamps())
|
|
16
|
+
.use(softDelete())
|
|
17
|
+
.use(tenant())
|
|
18
|
+
.use(actor())
|
|
19
|
+
.use(validation({ schemas: validators }));
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Plugins act on tables by the flags codegen writes. Turn them on in
|
|
23
|
+
`better-supabase.config.ts`, then rerun `gen`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
plugins: {
|
|
27
|
+
timestamps: true, // created_at, updated_at
|
|
28
|
+
softDelete: { column: 'archived_at' }, // default deleted_at
|
|
29
|
+
tenant: { column: 'organization_id' },
|
|
30
|
+
actor: true, // created_by, updated_by
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A table without the columns is left alone, and the types follow:
|
|
35
|
+
`restore()` and `withDeleted` only exist on soft-delete tables.
|
|
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 |
|
|
45
|
+
|
|
46
|
+
## Order
|
|
47
|
+
|
|
48
|
+
Hooks run in `use()` order, with two exceptions. `softDelete()` runs first,
|
|
49
|
+
so `timestamps()` and `actor()` stamp a soft delete as the update it
|
|
50
|
+
becomes. `validation()` runs last, so it sees the row after `tenant()`
|
|
51
|
+
filled it.
|
|
52
|
+
|
|
53
|
+
RLS still decides what a user can read and write. `tenant()` makes queries
|
|
54
|
+
explicit and fills the column; it doesn't replace the policy.
|
|
55
|
+
|
|
56
|
+
Docs: https://bettersupabase.com/docs/plugins.md
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start with `pnpm better-supabase doctor`. It checks the database, RLS,
|
|
4
|
+
`supabase/config.toml` and env files, and links every finding to its fix.
|
|
5
|
+
|
|
6
|
+
## By `DbError` kind
|
|
7
|
+
|
|
8
|
+
Branch on `result.error.kind`, not on the message.
|
|
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 |
|
|
25
|
+
|
|
26
|
+
## Other symptoms
|
|
27
|
+
|
|
28
|
+
- Types don't match the database: run `pnpm better-supabase gen`, then
|
|
29
|
+
`gen --check` in CI.
|
|
30
|
+
- 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.
|
|
33
|
+
- 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`).
|
|
35
|
+
- Tests pass with the service role but fail as a user: that's the RLS policy.
|
|
36
|
+
Test as users, never with the service role.
|
|
37
|
+
|
|
38
|
+
Docs: https://bettersupabase.com/docs/cli/doctor.md and
|
|
39
|
+
https://bettersupabase.com/docs/guides/limitations.md
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# APIs with better-supabase
|
|
7
|
+
|
|
8
|
+
Every adapter resolves the caller once and hands you `{ auth, db, supabase }`.
|
|
9
|
+
`auth.kind` is `user`, `anon`, `service` or `invalid`.
|
|
10
|
+
|
|
11
|
+
## Workflow: add an endpoint
|
|
12
|
+
|
|
13
|
+
1. Pick the adapter for the runtime (table below). Setup code for each is in
|
|
14
|
+
[references/adapters.md](references/adapters.md).
|
|
15
|
+
2. Decide who may call it with `allow` (see below).
|
|
16
|
+
3. Use `db` from the handler context and return its `Result`, a plain value
|
|
17
|
+
or a `Response`.
|
|
18
|
+
4. For a table exposed as REST, prefer `bs.resource(...)` over hand-written
|
|
19
|
+
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).
|
|
22
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Who gets in
|
|
28
|
+
|
|
29
|
+
`allow` defaults to `['user']`. Use `['user', 'anon']` for public endpoints
|
|
30
|
+
and `['service']` for machine callers. Rejected callers get a 401 with a
|
|
31
|
+
`WWW-Authenticate` header, or a 403, as Problem Details.
|
|
32
|
+
|
|
33
|
+
## Adapters
|
|
34
|
+
|
|
35
|
+
| Where | Setup | Handler |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Next.js | `createNext(sb)` in `lib/supabase.server.ts` | `next.route((req, { db }) => ...)`, `next.action({ input: schema }, (input, { db }) => ...)` |
|
|
38
|
+
| Hono | `createHono(sb)`, `.use('/api/*', bs.middleware())` | `c.var.db`; `bs.resource('customers', {...})` for REST |
|
|
39
|
+
| oRPC | `createOrpc(sb)`, `base.use(bs.middleware())` | `bs.unwrap(context.db.customers.findMany(...))` |
|
|
40
|
+
| Edge Functions | `createEdge(sb, { cors: true })` | `Deno.serve(bs.handler((req, { db }) => ...))` |
|
|
41
|
+
| MCP | `createMcp(sb, { name, version, resources })` | `.tool({ name, input, run: (args, { db }) => ... })` |
|
|
42
|
+
|
|
43
|
+
Don't build error JSON by hand; errors become Problem Details.
|
|
44
|
+
|
|
45
|
+
## REST resources
|
|
46
|
+
|
|
47
|
+
`defineResource` / `bs.resource(table, { operations, list, input })` gives you
|
|
48
|
+
list, get, create, update and delete, with validation and paging (`{ items, page }`).
|
|
49
|
+
`createOpenApi(sb, { resources })` describes the same routes, and
|
|
50
|
+
`better-supabase openapi emit --check` keeps `openapi.json` in sync.
|
|
51
|
+
|
|
52
|
+
## Background work (`better-supabase/jobs`, needs `sql add jobs idempotency webhook-inbox`)
|
|
53
|
+
|
|
54
|
+
- `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
|
+
- `createIdempotency(postgres.admin).handle(request, handler)` for POST endpoints that clients retry.
|
|
56
|
+
- `createInbox(postgres.admin, { source, secrets }).receive(request)` for webhooks; `process(handler)` later.
|
|
57
|
+
|
|
58
|
+
Done when a failing job is retried and then archived after `maxAttempts`,
|
|
59
|
+
and a replayed webhook or POST doesn't run twice.
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
Use `localAuth(secret)` as a resolver and `signTestJwt` / `asUser` from
|
|
64
|
+
`better-supabase/testing`. See the `better-supabase-testing` skill.
|
|
65
|
+
|
|
66
|
+
Docs: https://bettersupabase.com/docs/frameworks/hono.md (and `next`,
|
|
67
|
+
`orpc`, `edge`, `mcp` under `/docs/frameworks/`).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Adapter setup
|
|
2
|
+
|
|
3
|
+
Each adapter wraps the same `sb` from `src/lib/supabase.ts`:
|
|
4
|
+
|
|
5
|
+
```ts title="src/lib/supabase.ts"
|
|
6
|
+
import { defineSupabase } from 'better-supabase';
|
|
7
|
+
|
|
8
|
+
import { schema } from './supabase/generated';
|
|
9
|
+
|
|
10
|
+
export type { Functions, Models } from './supabase/generated';
|
|
11
|
+
|
|
12
|
+
export const sb = defineSupabase(schema);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Next.js
|
|
16
|
+
|
|
17
|
+
```ts title="src/lib/supabase.server.ts"
|
|
18
|
+
import { createNext } from 'better-supabase/next';
|
|
19
|
+
|
|
20
|
+
import { sb } from './supabase';
|
|
21
|
+
|
|
22
|
+
export const next = createNext(sb);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts title="src/proxy.ts"
|
|
26
|
+
import type { NextRequest } from 'next/server';
|
|
27
|
+
|
|
28
|
+
import { next } from './lib/supabase.server';
|
|
29
|
+
|
|
30
|
+
export const proxy = (request: NextRequest) => next.proxy(request);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The proxy is the only place that refreshes sessions. Server Components,
|
|
34
|
+
route handlers and actions read the verified token:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const { db } = await next.server();
|
|
38
|
+
export const GET = next.route((request, { db }) => db.customers.findMany({ limit: 20 }));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Hono
|
|
42
|
+
|
|
43
|
+
```ts title="src/server.ts"
|
|
44
|
+
import { type BetterEnv, createHono } from 'better-supabase/hono';
|
|
45
|
+
import { Hono } from 'hono';
|
|
46
|
+
|
|
47
|
+
import { type Functions, type Models, sb } from './lib/supabase';
|
|
48
|
+
|
|
49
|
+
const bs = createHono(sb);
|
|
50
|
+
|
|
51
|
+
const app = new Hono<BetterEnv<Models, Functions, unknown>>()
|
|
52
|
+
.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
|
+
|
|
57
|
+
export default app;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## oRPC
|
|
61
|
+
|
|
62
|
+
```ts title="src/router.ts"
|
|
63
|
+
import { os } from '@orpc/server';
|
|
64
|
+
import { createOrpc, type OrpcRequestContext } from 'better-supabase/orpc';
|
|
65
|
+
|
|
66
|
+
import { sb } from './lib/supabase';
|
|
67
|
+
|
|
68
|
+
export const bs = createOrpc(sb);
|
|
69
|
+
const authed = os.$context<OrpcRequestContext>().use(bs.middleware());
|
|
70
|
+
|
|
71
|
+
export const router = {
|
|
72
|
+
customers: authed.handler(({ context }) =>
|
|
73
|
+
bs.unwrap(context.db.customers.findMany({ limit: 20 })),
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Edge Functions
|
|
79
|
+
|
|
80
|
+
```ts title="supabase/functions/api/index.ts"
|
|
81
|
+
import { createEdge } from 'better-supabase/edge';
|
|
82
|
+
|
|
83
|
+
import { sb } from '../_shared/supabase.ts';
|
|
84
|
+
|
|
85
|
+
const bs = createEdge(sb, { cors: true });
|
|
86
|
+
|
|
87
|
+
Deno.serve(
|
|
88
|
+
bs.resources({ customers: { select: ['id', 'name'] } }, { basePath: '/api' }),
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## MCP
|
|
93
|
+
|
|
94
|
+
```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'] } },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
Deno.serve(mcp.fetch);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Tools run as the calling user, so RLS applies to every tool call.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: better-supabase-testing
|
|
3
|
+
description: Test Supabase RLS policies, APIs and SQL against the local stack with better-supabase/testing, typed seeds and pgTAP. Use when writing or fixing tests that touch the database, policies or auth.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing with better-supabase
|
|
7
|
+
|
|
8
|
+
Test against the local stack (`supabase start`) as real users. Don't mock
|
|
9
|
+
supabase-js, and don't use the service role for anything a user does.
|
|
10
|
+
|
|
11
|
+
## Workflow: test a table or policy
|
|
12
|
+
|
|
13
|
+
1. Start the stack (`supabase start`) and write `.env.local` with
|
|
14
|
+
`better-supabase env`.
|
|
15
|
+
2. Add the rows the test needs to `supabase/seed.ts`, including one row
|
|
16
|
+
owned by another tenant, then `supabase db reset`.
|
|
17
|
+
3. Write the RLS test with `asUser` for each role that matters.
|
|
18
|
+
4. Assert both directions: the user sees and changes their own rows, and
|
|
19
|
+
another tenant's rows come back as `not_found` or `forbidden`.
|
|
20
|
+
5. Run `supabase test db` if the project has pgTAP tests.
|
|
21
|
+
|
|
22
|
+
Done when the test fails after you drop or loosen the policy, and passes
|
|
23
|
+
again once you restore it.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
- `better-supabase env` writes the URL and keys to `.env.local`; load it in the test setup.
|
|
28
|
+
- Typed fixtures live in `supabase/seed.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { defineSeed } from 'better-supabase/testing';
|
|
32
|
+
import { sb } from '../src/lib/supabase.ts';
|
|
33
|
+
|
|
34
|
+
export const seed = defineSeed(sb, {
|
|
35
|
+
organizations: { acme: { id: ACME, name: 'Acme' } },
|
|
36
|
+
customers: { first: { id: FIRST, organizationId: ACME, name: 'First' } },
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`better-supabase seed` renders them to SQL for `supabase db reset`, and
|
|
41
|
+
tests import the same rows (`seed.rows.customers.first.id`).
|
|
42
|
+
|
|
43
|
+
## RLS tests
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const alice = await asUser(sb, { sub: aliceId, org_id: ACME }, { postgres });
|
|
47
|
+
expect(await alice.db.customers.count().orThrow()).toBe(1);
|
|
48
|
+
expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok: false });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Check both directions: the user sees their own rows and never sees another tenant's.
|
|
52
|
+
- `alice.sql` runs the same checks over direct Postgres.
|
|
53
|
+
- Assert on `result.error.kind` (`not_found`, `forbidden`, `conflict`, `validation`), not on messages.
|
|
54
|
+
|
|
55
|
+
## API tests
|
|
56
|
+
|
|
57
|
+
Pass `auth: { resolvers: [localAuth(LOCAL_JWT_SECRET)] }` to the adapter and
|
|
58
|
+
send `authorization: Bearer ${alice.token}`.
|
|
59
|
+
|
|
60
|
+
## pgTAP
|
|
61
|
+
|
|
62
|
+
`better-supabase sql add pgtap` installs `tests.create_user`,
|
|
63
|
+
`tests.authenticate_as(user, claims)`, `tests.clear_authentication()` and
|
|
64
|
+
`tests.rls_enabled('public')` for `supabase test db`. Put
|
|
65
|
+
`select tests.rls_enabled('public');` in a test so tables without RLS fail CI.
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|