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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -1
  3. package/bin/better-supabase.js +3 -0
  4. package/dist/casing-da0uRTqt.js +14 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +15 -0
  7. package/dist/cli/index.d.ts +226 -0
  8. package/dist/cli/index.js +3 -0
  9. package/dist/cli-CoByWYja.js +6472 -0
  10. package/dist/client/index.d.ts +2 -0
  11. package/dist/client/index.js +112 -0
  12. package/dist/compiler-DL18ukPe.d.ts +14 -0
  13. package/dist/config/index.d.ts +3 -0
  14. package/dist/config/index.js +3 -0
  15. package/dist/config-D-aR2ZoN.js +276 -0
  16. package/dist/define-CTfWw-SS.d.ts +662 -0
  17. package/dist/define-QGkq04oe.js +1068 -0
  18. package/dist/edge/index.d.ts +43 -0
  19. package/dist/edge/index.js +103 -0
  20. package/dist/entitlements-BBg61DQZ.d.ts +20 -0
  21. package/dist/entitlements-BC_khwRy.js +17 -0
  22. package/dist/env/index.d.ts +2 -0
  23. package/dist/env/index.js +2 -0
  24. package/dist/env-BEToab0I.js +204 -0
  25. package/dist/errors-Bl3rkOzo.js +196 -0
  26. package/dist/events/index.d.ts +2 -0
  27. package/dist/events/index.js +2 -0
  28. package/dist/events-2OJsVG_d.d.ts +84 -0
  29. package/dist/events-BuzTgn2D.js +135 -0
  30. package/dist/executor-Bazhgsgq.d.ts +195 -0
  31. package/dist/executor-Bje4suQF.d.ts +25 -0
  32. package/dist/executor-Ceq60hPM.js +89 -0
  33. package/dist/hono/index.d.ts +45 -0
  34. package/dist/hono/index.js +70 -0
  35. package/dist/index-BD6wKthp.d.ts +597 -0
  36. package/dist/index-DQVvHzjW.d.ts +74 -0
  37. package/dist/index-DRid0qIv.d.ts +76 -0
  38. package/dist/index-Dup-nSe9.d.ts +117 -0
  39. package/dist/index-Dy6kuF0P.d.ts +71 -0
  40. package/dist/index-Hf8PP9sP.d.ts +116 -0
  41. package/dist/index-JXHBuS53.d.ts +144 -0
  42. package/dist/index-Nxo3TJcm.d.ts +208 -0
  43. package/dist/index-Sw43J8HG.d.ts +62 -0
  44. package/dist/index-cEeweTeM.d.ts +38 -0
  45. package/dist/index.d.ts +110 -0
  46. package/dist/index.js +42 -0
  47. package/dist/invalidate-DbxuL2jL.js +18 -0
  48. package/dist/jobs/index.d.ts +173 -0
  49. package/dist/jobs/index.js +477 -0
  50. package/dist/json-schema-BbwO2mZg.js +181 -0
  51. package/dist/lint/index.d.ts +75 -0
  52. package/dist/lint/index.js +167 -0
  53. package/dist/list/index.d.ts +2 -0
  54. package/dist/list/index.js +410 -0
  55. package/dist/live-6b9i72Ux.d.ts +180 -0
  56. package/dist/live-D8rV1Ldw.js +168 -0
  57. package/dist/mcp/index.d.ts +105 -0
  58. package/dist/mcp/index.js +330 -0
  59. package/dist/mfa-CpogP66z.js +521 -0
  60. package/dist/mfa-TVia554c.d.ts +44 -0
  61. package/dist/next/image/index.d.ts +34 -0
  62. package/dist/next/image/index.js +40 -0
  63. package/dist/next/index.d.ts +210 -0
  64. package/dist/next/index.js +426 -0
  65. package/dist/openapi/index.d.ts +2 -0
  66. package/dist/openapi/index.js +332 -0
  67. package/dist/orpc/index.d.ts +46 -0
  68. package/dist/orpc/index.js +50 -0
  69. package/dist/otel/index.d.ts +40 -0
  70. package/dist/otel/index.js +168 -0
  71. package/dist/path-Cv__-OTj.d.ts +17 -0
  72. package/dist/plugin-BqR6wKMB.js +23 -0
  73. package/dist/plugin-HvGqCurB.d.ts +118 -0
  74. package/dist/plugins/actor/index.d.ts +13 -0
  75. package/dist/plugins/actor/index.js +43 -0
  76. package/dist/plugins/rules/index.d.ts +88 -0
  77. package/dist/plugins/rules/index.js +174 -0
  78. package/dist/plugins/soft-delete/index.d.ts +38 -0
  79. package/dist/plugins/soft-delete/index.js +69 -0
  80. package/dist/plugins/tenant/index.d.ts +43 -0
  81. package/dist/plugins/tenant/index.js +73 -0
  82. package/dist/plugins/timestamps/index.d.ts +9 -0
  83. package/dist/plugins/timestamps/index.js +37 -0
  84. package/dist/plugins/validation/index.d.ts +24 -0
  85. package/dist/plugins/validation/index.js +59 -0
  86. package/dist/pool-BFeqzwQS.d.ts +37 -0
  87. package/dist/postgres/index.d.ts +28 -0
  88. package/dist/postgres/index.js +79 -0
  89. package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
  90. package/dist/problem-DPu6Fh-t.js +97 -0
  91. package/dist/problem-DlgB0lYl.d.ts +47 -0
  92. package/dist/query/index.d.ts +2 -0
  93. package/dist/query/index.js +3 -0
  94. package/dist/query-ChvDbsDE.js +186 -0
  95. package/dist/react/index.d.ts +7 -0
  96. package/dist/react/index.js +5 -0
  97. package/dist/react/server.d.ts +13 -0
  98. package/dist/react/server.js +25 -0
  99. package/dist/react/session.d.ts +2 -0
  100. package/dist/react/session.js +3 -0
  101. package/dist/react-CrOvrnIP.js +241 -0
  102. package/dist/read-set-DhX2d04c.js +216 -0
  103. package/dist/realtime/index.d.ts +3 -0
  104. package/dist/realtime/index.js +253 -0
  105. package/dist/resolve-DW8h9NPm.d.ts +155 -0
  106. package/dist/resource-CYiu6Ygi.js +175 -0
  107. package/dist/resource-SkPTRJDD.d.ts +65 -0
  108. package/dist/respond-4-dhoV7x.js +380 -0
  109. package/dist/respond-PoTpy5DT.d.ts +169 -0
  110. package/dist/result-CTkCZ6JA.d.ts +198 -0
  111. package/dist/result-DWXatkd6.js +103 -0
  112. package/dist/scope-CDHcS7dE.js +90 -0
  113. package/dist/seed-CzecODcJ.js +101 -0
  114. package/dist/server/index.d.ts +42 -0
  115. package/dist/server/index.js +68 -0
  116. package/dist/session-C3osL21k.d.ts +21 -0
  117. package/dist/session-CD-3orO_.js +19 -0
  118. package/dist/shared-_6-uPL7E.js +57 -0
  119. package/dist/simplify-oHiDKRVQ.js +665 -0
  120. package/dist/spec-pins-CJPVghHy.d.ts +20 -0
  121. package/dist/spec-pins-t6sPaHvl.js +20 -0
  122. package/dist/sql-M8l3fnS0.js +260 -0
  123. package/dist/ssr/index.d.ts +5 -0
  124. package/dist/ssr/index.js +4 -0
  125. package/dist/standard-CyhRzctb.d.ts +7 -0
  126. package/dist/standard-FUi3Z3-7.js +21 -0
  127. package/dist/stats-3iy8u3_P.js +566 -0
  128. package/dist/storage/index.d.ts +4 -0
  129. package/dist/storage/index.js +2 -0
  130. package/dist/storage-BWXVUXGk.js +461 -0
  131. package/dist/tables-rZxRxNPW.js +59 -0
  132. package/dist/template-BeOGnynM.js +93 -0
  133. package/dist/template-mbvlysOo.d.ts +6 -0
  134. package/dist/testing/index.d.ts +294 -0
  135. package/dist/testing/index.js +751 -0
  136. package/dist/types-6cYK9pbJ.js +29 -0
  137. package/dist/types-C2fslP1z.d.ts +195 -0
  138. package/dist/version-Cqq1m3UY.js +4 -0
  139. package/dist/view-B0ltUmTi.d.ts +43 -0
  140. package/dist/view-B6F9QbG-.js +35 -0
  141. package/dist/webhooks/index.d.ts +152 -0
  142. package/dist/webhooks/index.js +2 -0
  143. package/dist/webhooks-DbMTJJX2.js +139 -0
  144. package/package.json +246 -17
  145. package/schemas/config-v1.json +328 -0
  146. package/schemas/doctor-report-v1.json +105 -0
  147. package/schemas/snapshot-v2.json +1002 -0
  148. package/skills/better-supabase/SKILL.md +84 -0
  149. package/skills/better-supabase/references/plugins.md +56 -0
  150. package/skills/better-supabase/references/troubleshooting.md +39 -0
  151. package/skills/better-supabase-api/SKILL.md +67 -0
  152. package/skills/better-supabase-api/references/adapters.md +108 -0
  153. package/skills/better-supabase-testing/SKILL.md +65 -0
  154. 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 = {};