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/UPDATEa 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 sync | Trigger + queue, no scripts | Application-managed |
| Data filters | Native SQL on the row | Separate metadata filter |
| Ops surface | Your existing Postgres | A whole new system |
| Sweet spot | Up to ~10M well-indexed rows | 100M+ or exotic distance functions |
| Lock-in | Open table schema | Product-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
- Supabase automatic embeddings guide. https://supabase.com/docs/guides/ai/automatic-embeddings
- Supabase Vector (pgvector toolkit). https://supabase.com/modules/vector
- Announcement: automatic embeddings. https://supabase.com/blog/automatic-embeddings
- pgvector repository. https://github.com/pgvector/pgvector