JavaScript developers building agents with Supabase often have to assemble three separate pieces themselves: a vector-memory contract, PostgREST mutations, and a pgvector search RPC. The easy shortcut is a generic SQL-execution RPC, but exposing caller-provided SQL—especially through a security definer function—is not a pattern I think the Supabase community should encourage.
There is also a portability problem: if application code is written directly around one vector provider's API, moving between Supabase, local pgvector, or another managed backend becomes a rewrite instead of a configuration change.
AgentsKit is an open-source, modular agent toolkit for JavaScript. Its @agentskit/memory package now includes a Supabase adapter behind the same VectorMemory contract used by its other storage backends.
The Supabase path intentionally uses:
supabase.from(table).upsert(...) for writes;delete().in('id', ids) for deletion;match_agentskit_vectors RPC for similarity search;security invoker, a fixed return shape, metadata containment filters, a threshold, and a hard maximum of 100 results;Emerson Braun proposes documenting AgentsKit as a Supabase pgvector adapter for JavaScript agents. The current implementation uses a secure pattern avoiding arbitrary SQL execution and supports portability across backends. Cenk KURTOĞLU highlights potential security misconceptions regarding RLS and suggests improvements for tenant isolation. The proposal seeks guidance on the best documentation section for this contribution.
Strong direction, and the core argument holds: a security definer RPC that accepts caller-provided SQL is a pattern worth actively discouraging, and replacing it with a bounded function with a fixed signature and a clamped result count is strictly better.
One thing I would want settled before this becomes a docs entry, because a docs entry teaches the security model and not just the wiring — and item 3 of your proposed contribution is explicitly "RLS, credential, and security invoker guidance."
In this configuration RLS is not enforcing tenancy, and security invoker is not doing what a reader will assume it does.
The adapter connects with a service credential, and your validation notes the function is granted only to service_role. The service role bypasses RLS. So "RLS was enabled" is true and also not load-bearing: no policy on the vectors table is evaluated on this path. security invoker is the right choice, but its usual selling point — the function stays inside the caller's permissions — buys nothing when the caller holds all of them.
That leaves the tenant boundary resting entirely on one parameter:
filter jsonb default '{}'::jsonb
...
where vectors.metadata @> filter
@> against '{}' is satisfied by every row. So a call site that omits filter does not error and does not warn — it returns every tenant's vectors ranked by similarity, which then goes into a prompt and comes back paraphrased with no attribution.
Your validation confirms the boundary works when it is used ("JSON metadata filtering was honored"), which is a different property from the boundary being enforced. The test that separates them is calling search with no filter while two tenants have rows, and asserting it does not return both.
None of this argues against documenting the adapter. It argues that the docs entry should say plainly that tenancy here is enforced by the caller, not by the database — otherwise the combination of "RLS enabled", "granted only to service_role" and "security invoker" reads as three layers of database-side protection when it is really one layer of application-side discipline.
Two changes that would let the entry claim more:
Make scope required and non-JSON: a tenant_id text parameter with no default, separate from the free-form filter jsonb. A missing argument then fails at the call site instead of silently widening. Mirroring that in the TypeScript search options — filter required rather than optional — turns the same mistake into a compile error.
If the goal is genuinely database-enforced isolation, the path has to run under a role subject to RLS rather than service_role, plus alter table ... force row level security so the owner does not bypass it either. Heavier, and probably the right default only for the multi-tenant case — but it is the only version where "RLS protects you here" is accurate.
Happy to review the SQL in the docs PR if that is useful. The bounded-function pattern is worth having written down somewhere official; I would just rather it be written down with the boundary described accurately, since a docs entry is exactly what people copy without re-deriving.
@supabase/supabase-js;Current setup and security guide: https://github.com/AgentsKit-io/agentskit/blob/e47f30cf5a938dcf865e9342a41fcf9d7d378fd1/apps/docs-next/content/docs/data/memory/supabase-vector.mdx
I tested the frozen implementation against a disposable Supabase free project in sa-east-1:
1 and 0.993883748801337;topK, threshold, and JSON metadata filtering were honored;service_role, and the project and credentials were deleted after validation.If this is useful to Supabase users, I would like to contribute a concise documentation entry rather than change Supabase core or supabase-js. It would contain:
security invoker guidance;Would this fit best as an AI integration, a pgvector framework example, or another existing docs section? I am happy to follow the maintainers' preferred scope and location before opening a PR.