Skip to content
ProvidersSupabase

Supabase

Reference for the Supabase adapter, which reaches pgvector through supabase-js and a set of SQL functions the package ships.

Supabase Vector is pgvector inside Postgres. This adapter reaches it through supabase-js instead of a Postgres connection, so it runs anywhere fetch runs: Edge Functions, serverless handlers, and the browser. PostgREST has no syntax for order by embedding <=> $1, so every verb calls a SQL function that ships with the package.

import { createClient } from "@supabase/supabase-js";
import { createSupabaseStore } from "vecstore-sdk/supabase";

const store = createSupabaseStore({
  client: createClient(supabaseUrl, supabaseKey),
  schema: "public",
});

Install the client alongside the SDK:

bun add vecstore-sdk @supabase/supabase-js

schema is optional. Leave it out and every function falls back to current_schema(), which is public on Supabase. The tables do not have to sit in a schema that PostgREST exposes, because the adapter never reads them directly.

Install the SQL functions

Run sql/supabase.sql once per project, with a role that can create functions:

psql "$SUPABASE_DB_URL" -f node_modules/vecstore-sdk/sql/supabase.sql

The file starts with create extension if not exists vector. If your role cannot create extensions, enable vector from the Database page of the dashboard first, then run the file again.

Supabase reloads the PostgREST schema cache when the file finishes. If a call still returns { kind: "unsupported" } naming a vecstore_ function, reload the cache by hand:

notify pgrst, 'reload schema';

Which key to use

Every function is declared security invoker, so it runs with the privileges of the key you passed to createClient. createIndex enables row level security on the table it creates and adds no policy, so under the anon or authenticated role query, fetch, upsert, and delete see and change nothing until you write one. The service role bypasses row level security.

create policy "tenants read their namespace" on "docs"
  for select to authenticated
  using (namespace = (auth.jwt() ->> 'tenant'));

Tables created before version 0.1.3 have row level security off. Turn it on with alter table "docs" enable row level security;.

createIndex and deleteIndex run DDL. The anon and authenticated roles cannot create or drop tables, so those two calls return { kind: "unauthorized" } under a publishable key. Call them with the service role key from a trusted server, or create the tables in a migration and never call them at all. The install script revokes vecstore_create_index and vecstore_drop_index from anon and authenticated.

Table layout

createIndex creates the table below, an HNSW (Hierarchical Navigable Small World) index for the metric, and a GIN (Generalized Inverted Index) index on metadata:

create table "docs" (
  id text not null,
  namespace text not null default '',
  embedding vector(1536) not null,
  metadata jsonb not null default '{}'::jsonb,
  primary key (namespace, id)
);

This is the layout the pgvector adapter uses. A table either adapter created works with the other one.

An index name can be at most 49 bytes, so that the _embedding_idx and _metadata_idx index names stay under the 63-byte Postgres identifier limit. createIndex returns invalid_argument for a longer name.

pgvector's HNSW index holds at most 2,000 dimensions, so createIndex with a larger dimension returns { kind: "provider" } and creates nothing, the same as on pgvector.

Metric detection

vecstore_query reads the metric from the HNSW or IVFFlat index definition in pg_indexes and defaults to cosine when no index exists. The lookup happens inside the same function call as the search, so a query costs one request.

vecstore_query also turns on pgvector's iterative index scan for the call when the extension is 0.8.0 or later. Without it, an HNSW scan stops at 40 candidates before Postgres applies the namespace and the filter, and a query returns fewer than topK records on a large table. Run sql/supabase.sql again after you upgrade the package to get this version of the function. The pgvector page describes the limits that remain.

listIndexes returns the tables in the schema that have an embedding vector column.

How filters travel

A filter goes over the wire as the JSON tree you built with eq, and, and the other builders. vecstore_filter_sql walks that tree and emits the same JSONB predicates the pgvector adapter compiles, with every value passed through quote_literal. Filter results on Supabase and pgvector match for the same data.

The SQL functions

The adapter calls seven functions:

FunctionCalled by
vecstore_create_indexcreateIndex
vecstore_drop_indexdeleteIndex
vecstore_list_indexeslistIndexes
vecstore_upsertupsert, in batches of 100 records
vecstore_queryquery
vecstore_fetchfetch, in batches of 500 ids
vecstore_deletedelete

Four more functions support them: vecstore_table quotes an index name into a table reference, vecstore_filter_sql compiles a filter, vecstore_contains_any writes the containment test that eq and isIn share, and vecstore_metric reads the metric from the index definition.

What the adapter stores

ConcernSupabase
IndexTable
Namespacenamespace column, part of the primary key
Idid text
Metadatametadata jsonb
MetricRead from the HNSW index operator class

Errors specific to Supabase

Two error kinds mean something specific here. A missing or outdated SQL install returns { kind: "unsupported" } carrying the function name in feature, because PostgREST answers PGRST202 for a function it cannot find. An expired or rejected key returns { kind: "unauthorized" }. Everything else follows the SQLSTATE mapping the pgvector adapter uses.

On this page