better-supabase 0.1.0 → 0.3.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 +34 -31
- package/dist/base64-CQeB1eSf.js +42 -0
- package/dist/claims-CoTobNC-.js +25 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +4 -14
- package/dist/{compiler-DL18ukPe.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -3
- package/dist/config/index.js +5 -3
- package/dist/config-Be4GwsqI.js +129 -0
- package/dist/{index-BD6wKthp.d.ts → config-C1CYcm6o.d.ts} +75 -54
- package/dist/{config-D-aR2ZoN.js → config-DOsGYSiT.js} +14 -136
- package/dist/{define-QGkq04oe.js → define-DF341GDj.js} +133 -110
- package/dist/{define-CTfWw-SS.d.ts → define-DUbydmJa.d.ts} +32 -14
- package/dist/edge/index.d.ts +4 -4
- package/dist/edge/index.js +4 -4
- package/dist/entitlements-CTEBm7IB.js +20 -0
- package/dist/entitlements-DvJXoI02.d.ts +42 -0
- package/dist/env/index.js +203 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +139 -1
- package/dist/{events-2OJsVG_d.d.ts → events-Do8jXh4D.d.ts} +3 -3
- package/dist/{executor-Bje4suQF.d.ts → executor-B26lgbHD.d.ts} +2 -2
- package/dist/{executor-Ceq60hPM.js → executor-C57jx2pn.js} +7 -2
- package/dist/{executor-Bazhgsgq.d.ts → executor-DynmIWhq.d.ts} +2 -2
- package/dist/guards-BnzDvFrz.js +10 -0
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +4 -4
- package/dist/{index-Hf8PP9sP.d.ts → index-BNsABq7P.d.ts} +17 -13
- package/dist/{index-JXHBuS53.d.ts → index-CADlNHK-.d.ts} +27 -14
- package/dist/{index-Nxo3TJcm.d.ts → index-CTTMS6_1.d.ts} +27 -8
- package/dist/index-Cf-D2jk1.d.ts +50 -0
- package/dist/{index-Dup-nSe9.d.ts → index-CriFFJ_p.d.ts} +4 -4
- package/dist/{index-DQVvHzjW.d.ts → index-GV77NpRP.d.ts} +5 -4
- package/dist/{index-Sw43J8HG.d.ts → index-UJ-LUxtI.d.ts} +6 -6
- package/dist/{index-cEeweTeM.d.ts → index-kpgopLNv.d.ts} +4 -3
- package/dist/{index-DRid0qIv.d.ts → index-qriRM9cZ.d.ts} +7 -2
- package/dist/index.d.ts +13 -13
- package/dist/index.js +18 -8
- package/dist/jobs/index.d.ts +25 -24
- package/dist/jobs/index.js +19 -12
- package/dist/{json-schema-BbwO2mZg.js → json-schema--aOXAGfU.js} +9 -3
- package/dist/lint/index.js +1 -1
- package/dist/list/index.d.ts +2 -2
- package/dist/list/index.js +128 -93
- package/dist/{live-6b9i72Ux.d.ts → live-QE3UPbGm.d.ts} +27 -6
- package/dist/logger-glM28NVN.js +21 -0
- package/dist/mcp/index.d.ts +59 -15
- package/dist/mcp/index.js +229 -67
- package/dist/{mfa-CpogP66z.js → mfa-BxoR1RkN.js} +97 -22
- package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
- package/dist/next/index.d.ts +51 -38
- package/dist/next/index.js +31 -25
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +58 -35
- package/dist/orpc/index.d.ts +6 -6
- package/dist/orpc/index.js +3 -3
- package/dist/otel/index.d.ts +3 -3
- package/dist/otel/index.js +5 -3
- package/dist/{template-BeOGnynM.js → permdock-sql-Bta_G0v-.js} +32 -3
- package/dist/permdock-sql-DImJULY-.d.ts +17 -0
- package/dist/{plugin-HvGqCurB.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
- package/dist/plugins/actor/index.d.ts +1 -1
- package/dist/plugins/rules/index.d.ts +4 -3
- package/dist/plugins/rules/index.js +8 -15
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +2 -2
- package/dist/plugins/tenant/index.d.ts +10 -4
- package/dist/plugins/tenant/index.js +15 -13
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +1 -1
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/pool-B9qsLyFR.d.ts +71 -0
- package/dist/postgres/index.d.ts +10 -5
- package/dist/postgres/index.js +36 -16
- package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
- package/dist/{problem-DPu6Fh-t.js → problem-CGt1ADix.js} +14 -8
- package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +1 -1
- package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +3 -3
- package/dist/react/server.d.ts +5 -5
- package/dist/react/server.js +1 -1
- package/dist/react/session.d.ts +21 -2
- package/dist/react/session.js +19 -1
- package/dist/{react-CrOvrnIP.js → react-DNBYNazu.js} +28 -23
- package/dist/{read-set-DhX2d04c.js → read-set-D0qBcboM.js} +3 -3
- package/dist/realtime/index.d.ts +4 -2
- package/dist/realtime/index.js +30 -14
- package/dist/{resolve-DW8h9NPm.d.ts → resolve-BRciX2jK.d.ts} +25 -9
- package/dist/{resource-SkPTRJDD.d.ts → resource-CwVgiI5q.d.ts} +16 -7
- package/dist/{resource-CYiu6Ygi.js → resource-D30rp0GN.js} +38 -16
- package/dist/{respond-4-dhoV7x.js → respond-C44EZAJo.js} +38 -19
- package/dist/{respond-PoTpy5DT.d.ts → respond-DAkPIdTH.d.ts} +23 -17
- package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
- package/dist/server/index.d.ts +35 -10
- package/dist/server/index.js +38 -9
- package/dist/{simplify-oHiDKRVQ.js → simplify-LakqK1vH.js} +22 -28
- package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
- package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
- package/dist/sql/index.d.ts +139 -0
- package/dist/sql/index.js +1983 -0
- package/dist/{sql-M8l3fnS0.js → sql-BIgk12ew.js} +72 -16
- package/dist/ssr/index.d.ts +5 -5
- package/dist/ssr/index.js +3 -3
- package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
- package/dist/{stats-3iy8u3_P.js → stats-D78UQ8Yu.js} +8 -7
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.js +530 -1
- package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
- package/dist/temporal-BwshQk0W.js +27 -0
- package/dist/temporal-required-CMxeSVPo.js +19 -0
- package/dist/testing/index.d.ts +49 -16
- package/dist/testing/index.js +233 -66
- package/dist/{types-C2fslP1z.d.ts → types-DONv9SHN.d.ts} +68 -12
- package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
- package/dist/view-3Po_PVlr.d.ts +79 -0
- package/dist/{view-B6F9QbG-.js → view-wZ-9qIUz.js} +12 -2
- package/dist/webhooks/index.d.ts +19 -17
- package/dist/webhooks/index.js +1 -1
- package/dist/{webhooks-DbMTJJX2.js → webhooks-CAIFQSTM.js} +12 -10
- package/package.json +30 -30
- package/schemas/config-v1.json +80 -5
- package/schemas/doctor-report-v1.json +18 -1
- package/schemas/snapshot-v2.json +39 -0
- package/skills/better-supabase/SKILL.md +23 -11
- package/skills/better-supabase/references/plugins.md +28 -16
- package/skills/better-supabase/references/troubleshooting.md +15 -15
- package/skills/better-supabase-api/SKILL.md +9 -9
- package/skills/better-supabase-api/references/adapters.md +30 -25
- package/skills/better-supabase-testing/SKILL.md +15 -11
- package/bin/better-supabase.js +0 -3
- package/dist/casing-da0uRTqt.js +0 -14
- package/dist/cli/bin.d.ts +0 -1
- package/dist/cli/bin.js +0 -15
- package/dist/cli/index.d.ts +0 -226
- package/dist/cli/index.js +0 -3
- package/dist/cli-CoByWYja.js +0 -6472
- package/dist/entitlements-BBg61DQZ.d.ts +0 -20
- package/dist/entitlements-BC_khwRy.js +0 -17
- package/dist/env-BEToab0I.js +0 -204
- package/dist/events-BuzTgn2D.js +0 -135
- package/dist/pool-BFeqzwQS.d.ts +0 -37
- package/dist/seed-CzecODcJ.js +0 -101
- package/dist/session-C3osL21k.d.ts +0 -21
- package/dist/session-CD-3orO_.js +0 -19
- package/dist/storage-BWXVUXGk.js +0 -461
- package/dist/view-B0ltUmTi.d.ts +0 -43
package/schemas/config-v1.json
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"type": "object",
|
|
68
68
|
"additionalProperties": false,
|
|
69
69
|
"properties": {
|
|
70
|
-
"timestamptz": { "enum": ["string", "
|
|
70
|
+
"timestamptz": { "enum": ["string", "instant"], "default": "string" },
|
|
71
71
|
"int8": { "enum": ["number", "string", "bigint"], "default": "number" },
|
|
72
72
|
"numeric": { "enum": ["number", "string"], "default": "number" }
|
|
73
73
|
}
|
|
@@ -148,8 +148,7 @@
|
|
|
148
148
|
"type": "object",
|
|
149
149
|
"additionalProperties": false,
|
|
150
150
|
"properties": {
|
|
151
|
-
"column": { "type": "string" }
|
|
152
|
-
"claim": { "type": "string" }
|
|
151
|
+
"column": { "type": "string" }
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
154
|
]
|
|
@@ -169,6 +168,25 @@
|
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
},
|
|
171
|
+
"claims": {
|
|
172
|
+
"description": "Claim names shared by the SQL kit, codegen and runtime defaults (PermDock's claim contract).",
|
|
173
|
+
"type": "object",
|
|
174
|
+
"additionalProperties": false,
|
|
175
|
+
"properties": {
|
|
176
|
+
"tenant": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"description": "Top-level claim holding the active tenant id. Defaults to `tenant_id`."
|
|
179
|
+
},
|
|
180
|
+
"scope": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"description": "`scope` of the tenant entries in the `memberships` claim. Defaults to `tenant`."
|
|
183
|
+
},
|
|
184
|
+
"features": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Claim holding plan features per tenant. Defaults to `features`."
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
172
190
|
"buckets": {
|
|
173
191
|
"type": "object",
|
|
174
192
|
"additionalProperties": {
|
|
@@ -182,7 +200,33 @@
|
|
|
182
200
|
"type": "string",
|
|
183
201
|
"description": "Path template, e.g. `{orgId}/{customerId}/logo.webp`."
|
|
184
202
|
},
|
|
185
|
-
"policy": {
|
|
203
|
+
"policy": {
|
|
204
|
+
"oneOf": [
|
|
205
|
+
{ "enum": ["tenant", "owner", "public", "none"] },
|
|
206
|
+
{
|
|
207
|
+
"type": "object",
|
|
208
|
+
"description": "Policies that call PermDock's permitted_<scope>_ids or permdock_has helpers.",
|
|
209
|
+
"required": ["permdock", "scope"],
|
|
210
|
+
"additionalProperties": false,
|
|
211
|
+
"properties": {
|
|
212
|
+
"permdock": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"required": ["read", "write"],
|
|
215
|
+
"additionalProperties": false,
|
|
216
|
+
"properties": {
|
|
217
|
+
"read": { "type": "string" },
|
|
218
|
+
"list": { "type": "string" },
|
|
219
|
+
"write": { "type": "string" },
|
|
220
|
+
"delete": { "type": "string" }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"scope": { "type": "string" },
|
|
224
|
+
"segment": { "type": "integer", "minimum": 1 },
|
|
225
|
+
"schema": { "type": "string" }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
186
230
|
"fileSizeLimit": { "type": "string" },
|
|
187
231
|
"allowedMimeTypes": { "type": "array", "items": { "type": "string" } }
|
|
188
232
|
}
|
|
@@ -239,6 +283,36 @@
|
|
|
239
283
|
"description": "The tenant id column of that table.",
|
|
240
284
|
"type": "string",
|
|
241
285
|
"default": "id"
|
|
286
|
+
},
|
|
287
|
+
"permdock": {
|
|
288
|
+
"description": "With a PermDock manifest, read PermDock's memberships for this scope instead of better_supabase.memberships; false keeps the kit's table.",
|
|
289
|
+
"oneOf": [
|
|
290
|
+
{ "const": false },
|
|
291
|
+
{
|
|
292
|
+
"type": "object",
|
|
293
|
+
"additionalProperties": false,
|
|
294
|
+
"properties": {
|
|
295
|
+
"scope": { "type": "string", "default": "organization" }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
"permdock": {
|
|
303
|
+
"description": "Where PermDock's JSON outputs are.",
|
|
304
|
+
"type": "object",
|
|
305
|
+
"additionalProperties": false,
|
|
306
|
+
"properties": {
|
|
307
|
+
"manifest": {
|
|
308
|
+
"description": "What `permdock supabase inspect --out` writes.",
|
|
309
|
+
"type": "string",
|
|
310
|
+
"default": "permdock.manifest.json"
|
|
311
|
+
},
|
|
312
|
+
"catalog": {
|
|
313
|
+
"description": "What `permdock catalog` writes.",
|
|
314
|
+
"type": "string",
|
|
315
|
+
"default": "permissions.catalog.json"
|
|
242
316
|
}
|
|
243
317
|
}
|
|
244
318
|
},
|
|
@@ -314,7 +388,8 @@
|
|
|
314
388
|
},
|
|
315
389
|
"strict": { "type": "boolean" },
|
|
316
390
|
"sources": { "type": "array", "items": { "type": "string" } },
|
|
317
|
-
"policyHelperLimit": { "type": "integer", "minimum": 1 }
|
|
391
|
+
"policyHelperLimit": { "type": "integer", "minimum": 1 },
|
|
392
|
+
"claimsLimit": { "type": "integer", "minimum": 1 }
|
|
318
393
|
}
|
|
319
394
|
}
|
|
320
395
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://unpkg.com/better-supabase/schemas/doctor-report-v1.json",
|
|
4
4
|
"title": "better-supabase doctor report",
|
|
5
|
-
"description": "Output of `better-supabase doctor --
|
|
5
|
+
"description": "Output of `better-supabase doctor --json`.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["$schema", "version", "tool", "summary", "findings"],
|
|
@@ -42,6 +42,12 @@
|
|
|
42
42
|
"BS106",
|
|
43
43
|
"BS107",
|
|
44
44
|
"BS108",
|
|
45
|
+
"BS109",
|
|
46
|
+
"BS110",
|
|
47
|
+
"BS111",
|
|
48
|
+
"BS112",
|
|
49
|
+
"BS113",
|
|
50
|
+
"BS114",
|
|
45
51
|
"BS200",
|
|
46
52
|
"BS204",
|
|
47
53
|
"BS205",
|
|
@@ -52,6 +58,15 @@
|
|
|
52
58
|
"BS210",
|
|
53
59
|
"BS211",
|
|
54
60
|
"BS212",
|
|
61
|
+
"BS213",
|
|
62
|
+
"BS214",
|
|
63
|
+
"BS215",
|
|
64
|
+
"BS216",
|
|
65
|
+
"BS217",
|
|
66
|
+
"BS218",
|
|
67
|
+
"BS219",
|
|
68
|
+
"BS220",
|
|
69
|
+
"BS221",
|
|
55
70
|
"BS301",
|
|
56
71
|
"BS302",
|
|
57
72
|
"BS303",
|
|
@@ -64,6 +79,8 @@
|
|
|
64
79
|
"BS404",
|
|
65
80
|
"BS405",
|
|
66
81
|
"BS406",
|
|
82
|
+
"BS407",
|
|
83
|
+
"BS408",
|
|
67
84
|
"BS501",
|
|
68
85
|
"BS502"
|
|
69
86
|
]
|
package/schemas/snapshot-v2.json
CHANGED
|
@@ -690,6 +690,12 @@
|
|
|
690
690
|
"additionalProperties": {
|
|
691
691
|
"type": "string"
|
|
692
692
|
}
|
|
693
|
+
},
|
|
694
|
+
"execute": {
|
|
695
|
+
"type": "array",
|
|
696
|
+
"items": {
|
|
697
|
+
"type": "string"
|
|
698
|
+
}
|
|
693
699
|
}
|
|
694
700
|
}
|
|
695
701
|
}
|
|
@@ -766,6 +772,10 @@
|
|
|
766
772
|
"items": {
|
|
767
773
|
"type": "string"
|
|
768
774
|
}
|
|
775
|
+
},
|
|
776
|
+
"source": {
|
|
777
|
+
"type": "string",
|
|
778
|
+
"description": "The function body (`pg_proc.prosrc`)."
|
|
769
779
|
}
|
|
770
780
|
}
|
|
771
781
|
}
|
|
@@ -897,6 +907,12 @@
|
|
|
897
907
|
},
|
|
898
908
|
"partial": {
|
|
899
909
|
"type": "boolean"
|
|
910
|
+
},
|
|
911
|
+
"method": {
|
|
912
|
+
"type": "string"
|
|
913
|
+
},
|
|
914
|
+
"predicate": {
|
|
915
|
+
"type": ["string", "null"]
|
|
900
916
|
}
|
|
901
917
|
}
|
|
902
918
|
}
|
|
@@ -992,6 +1008,29 @@
|
|
|
992
1008
|
}
|
|
993
1009
|
}
|
|
994
1010
|
},
|
|
1011
|
+
"columnGrants": {
|
|
1012
|
+
"description": "Column-level insert and update grants to anon and authenticated. Absent in snapshots taken before they were introspected.",
|
|
1013
|
+
"type": "array",
|
|
1014
|
+
"items": {
|
|
1015
|
+
"type": "object",
|
|
1016
|
+
"additionalProperties": false,
|
|
1017
|
+
"required": ["column", "role", "privileges"],
|
|
1018
|
+
"properties": {
|
|
1019
|
+
"column": {
|
|
1020
|
+
"type": "string"
|
|
1021
|
+
},
|
|
1022
|
+
"role": {
|
|
1023
|
+
"type": "string"
|
|
1024
|
+
},
|
|
1025
|
+
"privileges": {
|
|
1026
|
+
"type": "array",
|
|
1027
|
+
"items": {
|
|
1028
|
+
"type": "string"
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
995
1034
|
"large": {
|
|
996
1035
|
"description": "Postgres estimates at least 10,000 rows; the unbounded-read lint rule reads it.",
|
|
997
1036
|
"const": true
|
|
@@ -11,13 +11,22 @@ The data layer is `sb = defineSupabase(schema)` in `src/lib/supabase.ts`.
|
|
|
11
11
|
|
|
12
12
|
## Workflow: change the schema
|
|
13
13
|
|
|
14
|
-
1. Write the migration
|
|
15
|
-
`supabase
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
1. Write the migration: edit `supabase/schemas/*.sql`, then run
|
|
15
|
+
`supabase db schema declarative sync -f <name>` (pg-delta;
|
|
16
|
+
`supabase db diff -f <name>` when `config.toml` has no
|
|
17
|
+
`[experimental.pgdelta] enabled = true`), or `supabase migration new` for
|
|
18
|
+
data and role settings.
|
|
19
|
+
2. For a new table in an exposed schema, in the same file: `enable row level
|
|
20
|
+
security`, a policy per role and command the app uses, and an index on
|
|
21
|
+
every column a policy filters by. List the table in `expose` in
|
|
22
|
+
`better-supabase.config.ts` and run `pnpm better-supabase sql add grants`:
|
|
23
|
+
new tables get no Data API grants on their own.
|
|
24
|
+
3. Apply it with `supabase db reset` (or `supabase migration up`).
|
|
25
|
+
4. Run `pnpm better-supabase gen`, then fix the type errors it surfaces.
|
|
26
|
+
The command comes from the `@better-supabase/cli` dev dependency.
|
|
27
|
+
5. Run `pnpm better-supabase doctor` and fix every error it reports (RLS off,
|
|
28
|
+
missing policies, missing grants, unindexed foreign keys, drift).
|
|
29
|
+
6. Commit the generated files. CI runs `better-supabase gen --check`.
|
|
21
30
|
|
|
22
31
|
Done when `gen --check` and `doctor` exit 0 and the project typechecks.
|
|
23
32
|
|
|
@@ -48,10 +57,10 @@ stay hidden.
|
|
|
48
57
|
|
|
49
58
|
```ts
|
|
50
59
|
const result = await db.customers.findMany({
|
|
51
|
-
select: [
|
|
52
|
-
where: { status:
|
|
53
|
-
include: { organization: { select: [
|
|
54
|
-
orderBy: { name:
|
|
60
|
+
select: ["id", "name"],
|
|
61
|
+
where: { status: "active", notes: { some: { kind: "call" } } },
|
|
62
|
+
include: { organization: { select: ["name"] } },
|
|
63
|
+
orderBy: { name: "asc" },
|
|
55
64
|
limit: 20,
|
|
56
65
|
});
|
|
57
66
|
if (!result.ok) return result; // DbError: kind, message, status, code
|
|
@@ -82,3 +91,6 @@ See [references/troubleshooting.md](references/troubleshooting.md) for each
|
|
|
82
91
|
https://bettersupabase.com/docs. Every page is also served as Markdown at
|
|
83
92
|
`https://bettersupabase.com/docs/<path>.md`, and
|
|
84
93
|
https://bettersupabase.com/llms.txt lists them all.
|
|
94
|
+
|
|
95
|
+
For Supabase itself (Auth, Storage, RLS, Postgres performance), install
|
|
96
|
+
Supabase's skills next to these: `npx skills add supabase/agent-skills`.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Plugins
|
|
2
2
|
|
|
3
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
|
|
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 "./supabase/generated.ts";
|
|
12
|
+
import { validators } from "./supabase/generated.zod.ts";
|
|
13
13
|
|
|
14
14
|
export const sb = defineSupabase(schema)
|
|
15
15
|
.use(timestamps())
|
|
@@ -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
|
|
|
@@ -53,4 +53,16 @@ filled it.
|
|
|
53
53
|
RLS still decides what a user can read and write. `tenant()` makes queries
|
|
54
54
|
explicit and fills the column; it doesn't replace the policy.
|
|
55
55
|
|
|
56
|
+
The tenant comes from the verified `tenant_id` claim (renamed with
|
|
57
|
+
`claims.tenant` in `better-supabase.config.ts`), then `app_metadata.tenant_id`.
|
|
58
|
+
Never read it from `user_metadata`, a URL or a request body. When the project
|
|
59
|
+
has a `permdock.config.ts`, PermDock's hook writes that claim and the
|
|
60
|
+
memberships; don't run `sql add tenant` there or call `membership_claims()`
|
|
61
|
+
from a hook. `sql add entitlements` is fine: `features` is not a PermDock
|
|
62
|
+
claim. Add `claims: { features: 'better_supabase.feature_claims' }` to
|
|
63
|
+
`supabase.hook` in `permdock.config.ts`, run `permdock supabase hook generate`,
|
|
64
|
+
and `hasEntitlement(session, ...)` works. Run `permdock supabase inspect --out`
|
|
65
|
+
first: with `permdock.manifest.json` present, the entitlements module reads
|
|
66
|
+
PermDock's `member_<scope>_ids` helpers and doesn't add `tenant`.
|
|
67
|
+
|
|
56
68
|
Docs: https://bettersupabase.com/docs/plugins.md
|
|
@@ -7,21 +7,21 @@ 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 `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
25
|
|
|
26
26
|
## Other symptoms
|
|
27
27
|
|
|
@@ -32,13 +32,13 @@ and `['service']` for machine callers. Rejected callers get a 401 with a
|
|
|
32
32
|
|
|
33
33
|
## Adapters
|
|
34
34
|
|
|
35
|
-
| Where
|
|
36
|
-
|
|
|
37
|
-
| Next.js
|
|
38
|
-
| Hono
|
|
39
|
-
| oRPC
|
|
40
|
-
| Edge Functions | `createEdge(sb, { cors: true })`
|
|
41
|
-
| MCP
|
|
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
42
|
|
|
43
43
|
Don't build error JSON by hand; errors become Problem Details.
|
|
44
44
|
|
|
@@ -60,8 +60,8 @@ and a replayed webhook or POST doesn't run twice.
|
|
|
60
60
|
|
|
61
61
|
## Testing
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
Sign tokens with `signLocalJwt` or `asUser` from `better-supabase/testing`;
|
|
64
|
+
the adapter verifies them against the local JWKS. See the `better-supabase-testing` skill.
|
|
65
65
|
|
|
66
66
|
Docs: https://bettersupabase.com/docs/frameworks/hono.md (and `next`,
|
|
67
67
|
`orpc`, `edge`, `mcp` under `/docs/frameworks/`).
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Each adapter wraps the same `sb` from `src/lib/supabase.ts`:
|
|
4
4
|
|
|
5
5
|
```ts title="src/lib/supabase.ts"
|
|
6
|
-
import { defineSupabase } from
|
|
6
|
+
import { defineSupabase } from "better-supabase";
|
|
7
7
|
|
|
8
|
-
import { schema } from
|
|
8
|
+
import { schema } from "./supabase/generated";
|
|
9
9
|
|
|
10
|
-
export type { Functions, Models } from
|
|
10
|
+
export type { Functions, Models } from "./supabase/generated";
|
|
11
11
|
|
|
12
12
|
export const sb = defineSupabase(schema);
|
|
13
13
|
```
|
|
@@ -15,17 +15,17 @@ export const sb = defineSupabase(schema);
|
|
|
15
15
|
## Next.js
|
|
16
16
|
|
|
17
17
|
```ts title="src/lib/supabase.server.ts"
|
|
18
|
-
import { createNext } from
|
|
18
|
+
import { createNext } from "better-supabase/next";
|
|
19
19
|
|
|
20
|
-
import { sb } from
|
|
20
|
+
import { sb } from "./supabase";
|
|
21
21
|
|
|
22
22
|
export const next = createNext(sb);
|
|
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 { next } from
|
|
28
|
+
import { next } from "./lib/supabase.server";
|
|
29
29
|
|
|
30
30
|
export const proxy = (request: NextRequest) => next.proxy(request);
|
|
31
31
|
```
|
|
@@ -35,24 +35,29 @@ route handlers and actions read the verified token:
|
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
const { db } = await next.server();
|
|
38
|
-
export const GET = next.route((request, { db }) =>
|
|
38
|
+
export const GET = next.route((request, { db }) =>
|
|
39
|
+
db.customers.findMany({ limit: 20 }),
|
|
40
|
+
);
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
## Hono
|
|
42
44
|
|
|
43
45
|
```ts title="src/server.ts"
|
|
44
|
-
import { type BetterEnv, createHono } from
|
|
45
|
-
import { Hono } from
|
|
46
|
+
import { type BetterEnv, createHono } from "better-supabase/hono";
|
|
47
|
+
import { Hono } from "hono";
|
|
46
48
|
|
|
47
|
-
import { type Functions, type Models, sb } from
|
|
49
|
+
import { type Functions, type Models, sb } from "./lib/supabase";
|
|
48
50
|
|
|
49
51
|
const bs = createHono(sb);
|
|
50
52
|
|
|
51
53
|
const app = new Hono<BetterEnv<Models, Functions, unknown>>()
|
|
52
54
|
.onError(bs.onError)
|
|
53
|
-
.use(
|
|
54
|
-
.get(
|
|
55
|
-
.route(
|
|
55
|
+
.use("/api/*", bs.middleware())
|
|
56
|
+
.get("/api/me", (c) => c.json({ kind: c.var.auth.kind }))
|
|
57
|
+
.route(
|
|
58
|
+
"/api/customers",
|
|
59
|
+
bs.resource("customers", { select: ["id", "name"] }),
|
|
60
|
+
);
|
|
56
61
|
|
|
57
62
|
export default app;
|
|
58
63
|
```
|
|
@@ -60,10 +65,10 @@ export default app;
|
|
|
60
65
|
## oRPC
|
|
61
66
|
|
|
62
67
|
```ts title="src/router.ts"
|
|
63
|
-
import { os } from
|
|
64
|
-
import { createOrpc, type OrpcRequestContext } from
|
|
68
|
+
import { os } from "@orpc/server";
|
|
69
|
+
import { createOrpc, type OrpcRequestContext } from "better-supabase/orpc";
|
|
65
70
|
|
|
66
|
-
import { sb } from
|
|
71
|
+
import { sb } from "./lib/supabase";
|
|
67
72
|
|
|
68
73
|
export const bs = createOrpc(sb);
|
|
69
74
|
const authed = os.$context<OrpcRequestContext>().use(bs.middleware());
|
|
@@ -78,28 +83,28 @@ export const router = {
|
|
|
78
83
|
## Edge Functions
|
|
79
84
|
|
|
80
85
|
```ts title="supabase/functions/api/index.ts"
|
|
81
|
-
import { createEdge } from
|
|
86
|
+
import { createEdge } from "better-supabase/edge";
|
|
82
87
|
|
|
83
|
-
import { sb } from
|
|
88
|
+
import { sb } from "../_shared/supabase.ts";
|
|
84
89
|
|
|
85
90
|
const bs = createEdge(sb, { cors: true });
|
|
86
91
|
|
|
87
92
|
Deno.serve(
|
|
88
|
-
bs.resources({ customers: { select: [
|
|
93
|
+
bs.resources({ customers: { select: ["id", "name"] } }, { basePath: "/api" }),
|
|
89
94
|
);
|
|
90
95
|
```
|
|
91
96
|
|
|
92
97
|
## MCP
|
|
93
98
|
|
|
94
99
|
```ts title="supabase/functions/mcp/index.ts"
|
|
95
|
-
import { createMcp } from
|
|
100
|
+
import { createMcp } from "better-supabase/mcp";
|
|
96
101
|
|
|
97
|
-
import { sb } from
|
|
102
|
+
import { sb } from "../_shared/supabase.ts";
|
|
98
103
|
|
|
99
104
|
const mcp = createMcp(sb, {
|
|
100
|
-
name:
|
|
101
|
-
version:
|
|
102
|
-
resources: { customers: { select: [
|
|
105
|
+
name: "crm",
|
|
106
|
+
version: "0.1.0",
|
|
107
|
+
resources: { customers: { select: ["id", "name"] } },
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
Deno.serve(mcp.fetch);
|
|
@@ -11,7 +11,8 @@ supabase-js, and don't use the service role for anything a user does.
|
|
|
11
11
|
## Workflow: test a table or policy
|
|
12
12
|
|
|
13
13
|
1. Start the stack (`supabase start`) and write `.env.local` with
|
|
14
|
-
`better-supabase env`.
|
|
14
|
+
`better-supabase env`. In a sandbox without Docker, start it with
|
|
15
|
+
`SUPABASE_EXPERIMENTAL_STACK=1` (the Supabase CLI's native stack).
|
|
15
16
|
2. Add the rows the test needs to `supabase/seed.ts`, including one row
|
|
16
17
|
owned by another tenant, then `supabase db reset`.
|
|
17
18
|
3. Write the RLS test with `asUser` for each role that matters.
|
|
@@ -25,27 +26,30 @@ again once you restore it.
|
|
|
25
26
|
## Setup
|
|
26
27
|
|
|
27
28
|
- `better-supabase env` writes the URL and keys to `.env.local`; load it in the test setup.
|
|
29
|
+
- `better-supabase keys` creates `supabase/signing_keys.json`. Set `signing_keys_path = "./signing_keys.json"` under `[auth]` in `supabase/config.toml`, gitignore the file and restart the stack. `asUser` signs ES256 tokens with it.
|
|
28
30
|
- Typed fixtures live in `supabase/seed.ts`:
|
|
29
31
|
|
|
30
32
|
```ts
|
|
31
|
-
import { defineSeed } from
|
|
32
|
-
import { sb } from
|
|
33
|
+
import { defineSeed } from "better-supabase/testing";
|
|
34
|
+
import { sb } from "../src/lib/supabase.ts";
|
|
33
35
|
|
|
34
36
|
export const seed = defineSeed(sb, {
|
|
35
|
-
organizations: { acme: { id: ACME, name:
|
|
36
|
-
customers: { first: { id: FIRST, organizationId: ACME, name:
|
|
37
|
+
organizations: { acme: { id: ACME, name: "Acme" } },
|
|
38
|
+
customers: { first: { id: FIRST, organizationId: ACME, name: "First" } },
|
|
37
39
|
});
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
`better-supabase seed` renders them to SQL for `supabase db reset`, and
|
|
43
|
+
tests import the same rows (`seed.rows.customers.first.id`).
|
|
42
44
|
|
|
43
45
|
## RLS tests
|
|
44
46
|
|
|
45
47
|
```ts
|
|
46
|
-
const alice = await asUser(sb, { sub: aliceId,
|
|
48
|
+
const alice = await asUser(sb, { sub: aliceId, tenant_id: ACME }, { postgres });
|
|
47
49
|
expect(await alice.db.customers.count().orThrow()).toBe(1);
|
|
48
|
-
expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({
|
|
50
|
+
expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({
|
|
51
|
+
ok: false,
|
|
52
|
+
});
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
- Check both directions: the user sees their own rows and never sees another tenant's.
|
|
@@ -54,8 +58,8 @@ expect(await alice.db.customers.findById(OTHER_ORG_CUSTOMER)).toMatchObject({ ok
|
|
|
54
58
|
|
|
55
59
|
## API tests
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
Send `authorization: Bearer ${alice.token}` (or a token from `signLocalJwt`).
|
|
62
|
+
The adapter verifies it against the local JWKS; no test-only resolver is needed.
|
|
59
63
|
|
|
60
64
|
## pgTAP
|
|
61
65
|
|
package/bin/better-supabase.js
DELETED
package/dist/casing-da0uRTqt.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
//#region src/casing/index.ts
|
|
2
|
-
/** `first_name` to `firstName`. Leaves already camel-cased names untouched. */
|
|
3
|
-
function toCamel(name) {
|
|
4
|
-
return name.replace(/_+([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
5
|
-
}
|
|
6
|
-
/** `firstName` to `first_name`. */
|
|
7
|
-
function toSnake(name) {
|
|
8
|
-
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
9
|
-
}
|
|
10
|
-
function applyCasing(name, casing) {
|
|
11
|
-
return casing === "camel" ? toCamel(name) : name;
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
export { toCamel as n, toSnake as r, applyCasing as t };
|
package/dist/cli/bin.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|