@exposify/supabase 0.1.0 → 0.3.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,7 +15,7 @@ root.
15
15
 
16
16
  - A Supabase project (local via the [Supabase CLI](https://supabase.com/docs/guides/cli), or hosted).
17
17
  - The Supabase CLI (`supabase` on your `PATH`) for `db push` / `functions deploy`.
18
- - Node or Yarn to run `npx agentify-supabase` (the CLI is a regular npm
18
+ - Node or Yarn to run `npx exposify-supabase` (the CLI is a regular npm
19
19
  package; the edge function itself runs on Deno and needs no local install).
20
20
 
21
21
  ## 1. Scaffold
@@ -23,13 +23,16 @@ root.
23
23
  From your Supabase project's root (the directory containing `supabase/`):
24
24
 
25
25
  ```bash
26
- npx agentify-supabase init
26
+ npx exposify-supabase init
27
27
  ```
28
28
 
29
- This writes two things and never overwrites an existing function file:
29
+ This is safe to re-run — it never overwrites an existing function file, and
30
+ never scaffolds a second copy of the migration if one's already there
31
+ (checked by filename, not content: migrations are meant to be immutable
32
+ once written, so a re-run skips rather than rewrites). It writes:
30
33
 
31
- - `supabase/migrations/<timestamp>_agentify_meta.sql` — a SQL migration that
32
- adds `agentify_meta()`, a `SECURITY DEFINER` function returning, per table,
34
+ - `supabase/migrations/<timestamp>_exposify_meta.sql` — a SQL migration that
35
+ adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
33
36
  whether RLS is enabled and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
34
37
  you've written. It only ever returns catalog metadata (table/column names,
35
38
  flags, comments) — never row data — which is what makes it safe to grant
@@ -39,8 +42,13 @@ This writes two things and never overwrites an existing function file:
39
42
  - `supabase/functions/mcp/docs/` — an empty directory for optional bundled
40
43
  docs (see "Knowledge base" below).
41
44
 
42
- `init` is interactive from there. If the Supabase CLI is on your `PATH` and
43
- you're running it in a real terminal, it asks:
45
+ If you run it somewhere that doesn't look like a Supabase project root (no
46
+ `supabase/config.toml`), it warns but still proceeds — worth double-checking
47
+ you're in the directory you think you are before continuing.
48
+
49
+ `init` is interactive from there (skips straight to the old fixed behavior
50
+ below if stdin isn't a real terminal — CI, a script, piped input). If the
51
+ Supabase CLI is on your `PATH`, it asks:
44
52
 
45
53
  ```
46
54
  Apply the migration and deploy the function to whatever Supabase project is
@@ -49,8 +57,8 @@ currently linked, now? [y/N]
49
57
 
50
58
  Answering **y** runs `supabase db push` then `supabase functions deploy mcp`
51
59
  for you, streaming their output. The default is **no** — pressing Enter, the
52
- Supabase CLI not being installed, or running `init` non-interactively (CI, a
53
- script, piped input) all skip straight to printing the commands instead:
60
+ Supabase CLI not being installed, or running `init` non-interactively all
61
+ skip straight to printing the commands instead:
54
62
 
55
63
  ```
56
64
  Run these to finish setup:
@@ -61,14 +69,51 @@ Run these to finish setup:
61
69
 
62
70
  That's deliberate: `db push`/`functions deploy` act on whatever project is
63
71
  *currently linked*, which may not be a throwaway one, so nothing gets pushed
64
- without an explicit yes. If you want to edit `writes`/`rpc`/`functions` (next
65
- section) before the first deploy, just say no here and run the printed
66
- commands yourself once you're done editing.
72
+ without an explicit yes.
73
+
74
+ ### The setup wizard
75
+
76
+ If `init` can reach the project you're setting up — either because it was
77
+ already deployed (a re-run), or because you say yes above and the deploy
78
+ succeeds — it walks through a short interactive wizard instead of leaving
79
+ you to hand-edit `writes`/`rpc`/comments yourself:
80
+
81
+ 1. **Anonymous sign-in.** `init` tries to auto-detect this via the project's
82
+ public `/auth/v1/settings` endpoint and asks you to confirm; if it can't
83
+ detect it, it just asks. If enabled, it adds a warning comment to the
84
+ generated `index.ts`: an anonymous Supabase Auth session gets
85
+ `role: authenticated` (not `role: anon`), so it passes any RLS policy
86
+ scoped to `authenticated` unless that policy explicitly excludes
87
+ `auth.jwt()->>'is_anonymous' = 'true'`. Worth knowing before granting
88
+ `writes` on anything an anonymous session could reach.
89
+ 2. **Per-table writes.** For every RLS-enabled table, asks which of
90
+ insert/update/delete to grant (`i`, `u`, `d`, comma-separated, or Enter
91
+ for none) — the resulting `writes` array is written for you instead of
92
+ left as a commented-out example.
93
+ 3. **RPC functions.** Lists Postgres functions PostgREST exposes and lets
94
+ you pick which to expose as tools.
95
+ 4. **Documentation.** For every table or column `exposify_meta()` reports
96
+ has no comment, offers to write one right there — the answers become a
97
+ `COMMENT ON TABLE`/`COMMENT ON COLUMN` migration
98
+ (`<timestamp>_exposify_comments.sql`) that gets applied alongside the
99
+ rest. This is what gives `query_<table>`/`insert_<table>`/etc. their tool
100
+ and parameter descriptions (see "Column/table descriptions" below) —
101
+ worth doing even for tables you're not granting writes on, since it's
102
+ the only thing that makes read tools self-explanatory to an agent.
103
+
104
+ If `supabase/functions/mcp/index.ts` already exists, none of this touches
105
+ it — your selections are printed as a config snippet to merge in by hand
106
+ instead, and the documentation migration still gets written either way.
107
+ Connects using `SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` (env vars or
108
+ `--url`/`--key` flags — **the service role key, not the anon key**; see the
109
+ note on introspection below) — without those, `init` can't reach anything to
110
+ introspect and falls back to the fixed scaffold-and-ask-to-deploy flow above,
111
+ same as before this wizard existed.
67
112
 
68
113
  ## 2. Decide what's exposed
69
114
 
70
115
  **Every RLS-enabled table is automatically readable.** A table only shows up
71
- at all if `agentify_meta()` reports RLS is turned on for it *and* PostgREST
116
+ at all if `exposify_meta()` reports RLS is turned on for it *and* PostgREST
72
117
  actually exposes it (the calling role has grants) — an RLS-less table is
73
118
  invisible, full stop, with no override. For each exposed table you get a
74
119
  `query_<table>` tool: typed filters (`eq`/`neq`/`gt`/`lt`/`like`), a `select`
@@ -122,7 +167,34 @@ Other options you can pass to `aiReady()`:
122
167
  | `docsAuth` | `"public"` | `"required"` hides `search_docs` until the caller sends *any* `Authorization` header (presence-only, not validated) |
123
168
  | `maxRows` | `100` | Hard cap applied to every `query_<table>` call |
124
169
  | `rateLimit` / `rateWindowMs` | `60` / `60000` | In-memory, per-instance, best-effort rate limit — **not** distributed across edge instances; real limits belong to Supabase's own gateway |
125
- | `supabaseUrl` / `anonKey` | read from `SUPABASE_URL` / `SUPABASE_ANON_KEY` env vars | Override for local dev/testing; in a deployed function the env vars are automatic |
170
+ | `supabaseUrl` / `anonKey` / `serviceRoleKey` | read from `SUPABASE_URL` / `SUPABASE_ANON_KEY` / `SUPABASE_SERVICE_ROLE_KEY` env vars | Override for local dev/testing; in a deployed function all three env vars are automatic. `serviceRoleKey` is used only for schema introspection — see below. |
171
+
172
+ ### Why introspection needs the service role key
173
+
174
+ Every actual data call (`query_<table>`, `insert_<table>`, `rpc_<fn>`, ...)
175
+ uses the caller's own forwarded JWT, or the anon key as its fallback for an
176
+ unauthenticated caller — **that part is unaffected by any of this** and is
177
+ still what makes RLS the real authorization layer.
178
+
179
+ Schema introspection is different: fetching PostgREST's OpenAPI document (the
180
+ one-time-per-instance call that discovers which tables/columns exist) has to
181
+ use `SUPABASE_SERVICE_ROLE_KEY`. Supabase's gateway rejects that specific
182
+ request outright for an anon or publishable-tier key —
183
+ `{"message":"Invalid API key","hint":"Only the service_role API key can be
184
+ used for this endpoint."}` — regardless of RLS policies or table grants, and
185
+ regardless of whether you're using a legacy JWT-format anon key or a newer
186
+ `sb_publishable_...` key. This isn't documented anywhere in Supabase's own
187
+ docs as far as we've found; it was discovered by deploying this SDK against
188
+ a real project. The service role key never leaves the function and is never
189
+ used for anything except this schema-shape discovery — it doesn't touch row
190
+ data, and RLS still fully governs every actual tool call.
191
+
192
+ If you deploy with `supabase functions deploy` this just works — Supabase
193
+ auto-injects `SUPABASE_SERVICE_ROLE_KEY` into every edge function already.
194
+ The only place you'd ever need to supply it yourself is local dev/testing
195
+ (the `serviceRoleKey` option above) or the CLI's `init`/`generate` (see
196
+ their own sections), which run outside the deployed function and need it
197
+ passed explicitly.
126
198
 
127
199
  ## 3. Apply the migration and deploy
128
200
 
@@ -136,7 +208,7 @@ supabase functions deploy mcp
136
208
 
137
209
  Only `functions deploy` is needed for a config-only change to `index.ts`;
138
210
  `db push` is only needed again if the migration itself changed (it hasn't,
139
- unless you're editing `sql/agentify_meta.sql` directly).
211
+ unless you're editing `sql/exposify_meta.sql` directly).
140
212
 
141
213
  (Local dev: `supabase start` then `supabase functions serve mcp` instead of
142
214
  `deploy`.)
@@ -169,9 +241,9 @@ them via the `search_docs` tool.
169
241
  Every request tries, in order:
170
242
 
171
243
  1. **Runtime introspection** — fetch PostgREST's OpenAPI document and call
172
- `agentify_meta()`. This is the normal path; the result is cached in
244
+ `exposify_meta()`. This is the normal path; the result is cached in
173
245
  memory for the life of the edge instance, not refetched per call.
174
- 2. **Bundled manifest** — if step 1 fails, and a `agentify.manifest.json`
246
+ 2. **Bundled manifest** — if step 1 fails, and a `exposify.manifest.json`
175
247
  was generated ahead of time (see below), tools are built from that
176
248
  instead.
177
249
  3. **Functions + knowledge base only** — if neither works, the function
@@ -188,31 +260,38 @@ project (local `supabase start` or linked):
188
260
  ```bash
189
261
  SUPABASE_URL=http://localhost:54321 \
190
262
  SUPABASE_SERVICE_ROLE_KEY=<service-role-key> \
191
- npx agentify-supabase generate
263
+ npx exposify-supabase generate
192
264
  ```
193
265
 
194
- This writes `supabase/functions/mcp/agentify.manifest.json`. Load it in
266
+ Uses the **service role key**, deliberately the same key runtime
267
+ introspection itself uses (see "Why introspection needs the service role
268
+ key" above, and "Fallback ladder" below) — the anon key can't fetch
269
+ PostgREST's OpenAPI document at all, so `generate` needs the same
270
+ privileged key runtime introspection does. Nothing from the service role
271
+ key ends up in the generated manifest — it's just table/column metadata.
272
+
273
+ This writes `supabase/functions/mcp/exposify.manifest.json`. Load it in
195
274
  `aiReady({ manifest: ... })` — since the function runs on Deno, import it
196
275
  with a JSON import:
197
276
 
198
277
  ```ts
199
- import manifest from "./agentify.manifest.json" with { type: "json" };
278
+ import manifest from "./exposify.manifest.json" with { type: "json" };
200
279
 
201
280
  Deno.serve(aiReady({ manifest, /* ...writes/rpc/functions... */ }));
202
281
  ```
203
282
 
204
283
  ## Troubleshooting
205
284
 
206
- **"agentify_meta() is not reachable" / "PGRST202" errors in function logs.**
285
+ **"exposify_meta() is not reachable" / "PGRST202" errors in function logs.**
207
286
  The migration hasn't been applied to this database. Run `supabase db push`
208
- (or apply `sql/agentify_meta.sql` from this package directly), then
287
+ (or apply `sql/exposify_meta.sql` from this package directly), then
209
288
  redeploy the function — the error message names this exact fix.
210
289
 
211
290
  **A table I expected doesn't show up as a tool.** Check, in order: is RLS
212
291
  actually enabled on it (`alter table ... enable row level security;`)? Does
213
292
  the calling role (`anon`/`authenticated`) have grants PostgREST can see? If
214
293
  both are true and it's still missing, confirm the migration is applied —
215
- `agentify_meta()` is what reports the RLS flag in the first place.
294
+ `exposify_meta()` is what reports the RLS flag in the first place.
216
295
 
217
296
  **An `rpc_<fn>` tool takes a single opaque `args` object instead of named
218
297
  parameters.** PostgREST's OpenAPI document didn't have a recognizable
@@ -226,3 +305,23 @@ entries are `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` /
226
305
  `"<table>.*"` — a malformed entry is dropped (with a console warning), not
227
306
  an error, so a typo silently disables just that grant rather than crashing
228
307
  the function.
308
+
309
+ **`init` just scaffolds and asks to deploy — it never asks about writes/RPCs/
310
+ documentation.** The wizard only runs when `init` can actually reach the
311
+ project (`SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` env vars, or `--url`/`--key`
312
+ flags — the service role key, not the anon key, for the same reason described
313
+ above — pointing at a project where the migration is already applied). On a
314
+ brand-new project's first `init`, that's not true yet — say yes to the
315
+ deploy prompt (or run `supabase db push` yourself) and the wizard picks up
316
+ automatically once the migration is live, in the same run if you said yes,
317
+ or on your next `exposify-supabase init` otherwise.
318
+
319
+ **`PostgREST OpenAPI fetch failed: HTTP 401`, and the error body (if you
320
+ curl it directly) says `"Only the service_role API key can be used for this
321
+ endpoint"`.** This is expected if `SUPABASE_SERVICE_ROLE_KEY` isn't
322
+ reaching the function — see "Why introspection needs the service role key"
323
+ above. On a deployed function this should be automatic; if you're still
324
+ seeing this after a `supabase functions deploy`, check `supabase secrets
325
+ list` for a stale explicit `SUPABASE_SERVICE_ROLE_KEY` override, and confirm
326
+ you're not accidentally passing an anon/publishable key as `serviceRoleKey`
327
+ in local dev.