Skip to content
Blog

AI-Native Databases: Auto-Embeddings and Data APIs

The AI-native database keeps embeddings and your data in sync without a separate pipeline. Learn the trigger + queue + edge-function pattern on Supabase pgvector, and when auto-embeddings beats a standalone vector store.

Published on August 8, 2026

AI Assistant

The classic RAG pain: you insert a row into Postgres, then write a script that embeds it, run that script on a schedule, reset the embedding when the text changes, and keep it in sync forever. Every step is a chance to drift. The “AI-native” database answers by moving the embedding lifecycle into the database: when a row changes, the database enqueues a job, and a queue-and-worker pipeline keeps the vector fresh automatically. Supabase calls this pattern automatic embeddings.

The pattern: triggers → pgmq → pg_net → pg_cron → edge function

The whole thing runs on Postgres primitives only:

  • pgvector — store and query the embedding vector.
  • pgmq — a transactional message queue holding embedding jobs.
  • pg_net — lets Postgres fire async HTTP requests to an edge function.
  • pg_cron — a scheduler that processes the queue every few seconds.
  • Triggers — on INSERT/UPDATE a row, enqueue an embedding job.

The important architectural point: the inference call itself stays with an external model (OpenAI, HuggingFace, or your self-hosted service). What becomes “a feature of the database” is the coordination — the DB owns keeping vectors fresh, so you never hand-sync again.

Step 1 — the table and the index

create table documents (
    id         integer primary key generated always as identity,
    title      text not null,
    content    text not null,
    embedding  halfvec(1536),
    created_at timestamptz default now()
);

create index on documents using hnsw (embedding halfvec_cosine_ops);

halfvec is half-precision — the vector is stored at half the size of a full float4. The HNSW index gives you fast approximate-nearest-neighbor search that stays quick even into the tens of millions of rows. (One constraint: HNSW supports at most 4000 dimensions for halfvec, so a higher-dim model must be trimmed — that’s what Matryoshka embeddings are for.)

Step 2 — the “what to embed” function

Tell the pipeline which substring of each row is the searchable text:

create function custom_documents_input(row documents)
returns text language sql immutable as $$
  select row.title || ' ' || left(row.content, 4000)
$$;

Step 3 — triggers that enqueue the job

create trigger embed_documents_on_insert
after insert on documents
for each row execute function util.queue_embeddings('custom_documents', 'embedding');

create trigger embed_documents_on_update
after update of title, content on documents
for each row execute function util.queue_embeddings('custom_documents', 'embedding');

Two details worth copying: the update trigger only fires when the content columns change (a last_viewed bump doesn’t re-embed), and the payload is serialized into the pgmq queue transactionally — either the write and the enqueue both commit, or neither does.

Step 4 — the worker (cron + edge function)

A pg_cron job pops unprocessed jobs from the queue every few seconds and fires the edge function via pg_net. That function calls your embedding API and writes the vector back:

async function generateEmbedding(text: string) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  })
  const [data] = response.data
  if (!data) throw new Error('embedding failed')
  return data.embedding
}

The failing case is free: a job that fails stays in the queue and retries on the next tick. Need to debug a stuck embedding? pgmq is just Postgres — inspect it with SQL, SELECT * FROM pgmq.arch(a.')-style calls, no extra service to babysit.

Querying: the vector is just a column

const { data } = await supabase.rpc('match_documents', {
  query_embedding: embedding,   // generated from end user query
  match_count: 5,
})

Because the embedding lives on the same row as your data, search joins back against that row with ordinary SQL: filter by tenant, ORDER BY created time, count across a year. No separate index, no cross-service reconciliation.

When auto-embeddings beats a standalone store

Supabase (auto-embeddings)Dedicated vector DB
Keeping vectors in syncTrigger + queue, no scriptsApplication-managed
Data filtersNative SQL on the rowSeparate metadata filter
Ops surfaceYour existing PostgresA whole new system
Sweet spotUp to ~10M well-indexed rows100M+ or exotic distance functions
Lock-inOpen table schemaProduct-specific index

If your Postgres is already the source of truth — and yours is — auto-embeddings removes an entire class of drift bugs. Only reach for a dedicated vector database when row count or query rate clearly outgrows a single Postgres instance.

Conclusion & Next Steps

Auto-embeddings makes the embedding lifecycle a database concern: trigger, enqueue, queued worker, vector in place — with retries for free. Next: point the edge function at a self-hosted model if you want zero cloud API dependency, use Matryoshka-reduced embeddings so you fit under the 4000-dim HNSW ceiling, wire your queries through a parameterized match_documents RPC filtered by tenant, and re-run the pipeline any time you swap to a stronger embedding model.

References / Sources