Keep Supabase embeddings in sync after writes
Supabase automatic embeddings workflow for keeping vector search updated from database inserts, updates and content changes.
Faisal Karkoh · 13 Oct 2026 · 10 min read
Embeddings drift when the text in your database changes but the vector column does not. Supabase Automatic Embeddings is useful when you want inserts and updates to create embedding work automatically, while the database write itself stays fast and does not wait on an external embedding API call.
The implementation is not mainly about prompts. It is a data consistency workflow: row change, queued job, asynchronous worker, row update, retry and verification. This guide uses one illustrative public.documents table throughout so the moving parts stay connected.
Evidence disclosure: this article is based only on the supplied Supabase and OpenAI documentation excerpts accessed on 2026-10-13. It does not include author-run tests, production benchmarks, client implementation evidence or executed code.
When Supabase Automatic Embeddings is the right pattern
Use this pattern when embeddings need to follow database changes continuously. Supabase lists Automatic Embeddings as General Availability and available for self-hosting, describing the feature as automated embedding generation using Postgres triggers, queues and Edge Functions, with asynchronous generation and retry logic for failed jobs on the Supabase Automatic Embeddings feature page.
Supabase’s public feature page establishes Automatic Embeddings as a GA pattern built around triggers, queues, Edge Functions, asynchronous generation and retry logic.
The key design choice is to keep embedding generation outside the original insert or update transaction. Supabase’s Automatic embeddings guide uses Supabase Edge Functions, pgmq, pg_net and pg_cron because semantic embeddings usually require asynchronous calls to a provider such as OpenAI rather than a synchronous database-only trigger.
This is a good fit for product data, documentation, help-centre articles or internal knowledge rows that change after initial creation. It is less useful for a one-off import of static content, where a controlled batch script may be simpler. If you are still deciding whether Supabase should be the backend for an AI product at all, the broader trade-offs are covered in Supabase review for AI builders. If the source text needs chunking, moderation, enrichment or other multi-step transformation before embedding, keep that preprocessing explicit rather than hiding it inside a per-row trigger.
The worked example: documents that re-embed on insert and content updates
The illustrative workload is a SaaS documentation app with a public.documents table. Each row has:
id, the primary key expected by the Supabase reference trigger pattern.
title, used as part of the embedding input.
content, also used as part of the embedding input.
embedding, the destination vector column updated by the worker.
The model assumption is OpenAI text-embedding-3-small. OpenAI’s embeddings guide states that text-embedding-3-small returns 1536 dimensions by default, while text-embedding-3-large returns 3072 by default. The Supabase guide uses halfvec(1536) for text-embedding-3-small and explains that halfvec stores half-precision 16-bit values to save space.
| Embedding choice | Documented default dimensions | Column implication |
OpenAI text-embedding-3-small | 1536 | Use a matching vector or halfvec(1536) column. |
OpenAI text-embedding-3-large | 3072 | Use a matching 3072-dimension column if you choose this model. |
Reduced dimensions for text-embedding-3 models | Chosen through the optional dimensions parameter | The column must match the reduced dimension value. |
halfvec with HNSW indexing | Not a model; a pgvector storage type | The Supabase guide warns that HNSW indexes support a maximum of 4000 dimensions for halfvec vectors. |
The expected flow is simple: a document row is inserted or its searchable text changes, a queue message is created, a scheduled processor calls an Edge Function, the Edge Function asks the embedding provider for a vector, the row is updated and the processed queue message is deleted. If processing fails within the queue visibility timeout, the message becomes visible again for a later retry.
Step 1: prepare the database extensions and schema
Before adding any trigger, enable the required database extensions and confirm the schema matches the selected embedding model. Supabase’s reference guide enables five extensions: vector for vector operations, pgmq for queueing jobs, pg_net for asynchronous HTTP requests, pg_cron for scheduled processing and retries, and hstore for clearing embeddings during updates.
The extension setup in the guide follows this shape:
create extension if not exists vector
create extension if not exists pgmq
create extension if not exists pg_net
create extension if not exists pg_cron
create extension if not exists hstore
There is one version check to make before you design around queues. Supabase’s Queues quickstart states that pgmq is available in Postgres version 15.6.1.143 or later for Supabase Postgres projects.
Now lock the model and column contract. If the worker generates a 1536-dimension embedding but the table expects a different size, the pipeline will fail at update time. For this example, the safe contract is text-embedding-3-small with halfvec(1536), matching the Supabase guide’s example. OpenAI’s Create embeddings API reference also says the optional dimensions parameter is supported only in text-embedding-3 and later models, so any deliberate dimension reduction must be reflected in the column definition. For production schema work, keep these changes in reviewed migrations rather than manual dashboard edits; the same principle is used in the Cursor and Supabase migration workflow.
Step 2: enqueue embedding jobs from row changes
The database trigger should not generate the embedding. Its job is to describe the work and put it on a queue.
In Supabase’s reference pattern, the trigger function sends a message to the embedding_jobs queue with five important fields:
id: the changed row’s id.
schema: the table schema, such as public.
table: the table name, such as documents.
contentFunction: the function that turns the row into embedding input.
embeddingColumn: the column to update, such as embedding.
The same guide says this trigger function is generic and reusable across tables, but it requires a for each row trigger and the example expects a primary key column named id. If your table uses another primary key shape, do not copy the reference trigger blindly; adapt the queue contract first.
Use separate insert and update triggers. The insert trigger enqueues work whenever a new document row is created. The update trigger should fire only when title or content changes, because Supabase’s guide creates an update trigger on those columns to avoid unnecessary embedding updates. Updating an unrelated field such as a view counter should not spend embedding tokens or create stale queue noise.
The content function is where you decide what text is embedded. For the example table, it could combine title and content into one input string. If you later need chunking, metadata enrichment or policy checks, treat that as a pipeline design change rather than a minor trigger edit.
Step 3: process the queue with pg_cron and an Edge Function
Once jobs are queued, a scheduled processor reads them in batches. Supabase’s guide uses a database function to process embedding jobs, schedules it with pg_cron, and calls an Edge Function asynchronously through pg_net. The reference Edge Function uses OpenAI’s API to generate embeddings, updates the corresponding database row, deletes the queue job after successful processing and requires OPENAI_API_KEY as an environment variable.
The Edge Function limits page is the public source for the worker constraints that shape batch size, timeout and scheduling decisions.
The guide schedules processing every 10 seconds as a starting point, not a universal production setting. It also notes that batch_size, max_requests and timeout_milliseconds should be tuned for the workload.
| Queue processing knob | What it controls | Why it matters |
batch_size | How many queue messages one processor run reads. | Larger batches reduce scheduling overhead but can increase worker duration and provider pressure. |
max_requests | How many asynchronous HTTP requests are made. | Controls concurrency between Postgres, pg_net, the Edge Function and the embedding provider. |
timeout_milliseconds | How long queue messages remain invisible while being processed. | If the worker does not finish successfully within the visibility timeout, the message can be retried later. |
| 10-second cron interval | The documented starting schedule in the Supabase guide. | Useful for responsive updates, but it should be adjusted for write volume and worker duration. |
Hosted Edge Function limits also affect worker sizing. Supabase’s Edge Functions limits page lists 256 MB maximum memory, 2 seconds CPU time per request, a 150-second request idle timeout, and plan-dependent wall-clock duration: 150 seconds on Free and 400 seconds on paid plans. Supabase’s Cron documentation says Cron uses the pg_cron Postgres extension and recommends no more than eight jobs running concurrently, with each job running no more than ten minutes.
| Limit or recommendation | Documented value | Implementation implication |
| Edge Function memory | 256 MB maximum | Avoid oversized batches or heavy in-worker preprocessing. |
| Edge Function CPU time | 2 seconds per request | Keep the worker mostly I/O-bound and avoid expensive local processing. |
| Edge Function idle timeout | 150 seconds | Do not wait indefinitely on provider calls. |
| Edge Function wall-clock duration | 150 seconds on Free, 400 seconds on paid plans | Batch sizing must leave room for provider latency and database updates. |
| Cron concurrency recommendation | No more than eight jobs concurrently | Avoid schedules that overlap under load. |
If OpenAI is not the desired provider, the Supabase guide says the generateEmbedding function can be replaced with another embedding generation path. Supabase’s generate embeddings quickstart separately states that built-in Edge Runtime inference currently supports only the gte-small text embedding model, so treat that as a different provider choice rather than a drop-in OpenAI model swap.
Step 4: handle stale embeddings, retries and failed jobs
The main consistency risk is the stale-embedding window. After a document’s title or content changes, the old embedding may remain in the row until the worker generates and writes the new one. Supabase documents this explicitly and describes an optional before-update trigger that clears the existing embedding so stale embeddings are not served while regeneration is pending.
The troubleshooting section shows what failed embedding work looks like in the documented response shape, including failedJobs and error details for diagnosis.
That choice is product-specific. If search can tolerate temporarily old results, keep the old embedding until the replacement is ready. If stale results are unacceptable, clear the embedding on relevant updates and make the search layer handle temporarily missing embeddings.
Retries come from the queue behaviour, not from wishful thinking in the app. Supabase’s guide relies on pgmq visibility timeouts: if the Edge Function does not successfully process a message within the timeout, the message becomes visible again and can be retried by the next scheduled task.
Failures also need to be visible. The reference embed Edge Function returns HTTP 200 with completedJobs and failedJobs arrays, includes an error field for failed jobs, and exposes x-completed-jobs and x-failed-jobs headers. The same Supabase troubleshooting section says pg_net stores HTTP responses in net._http_response, which can be queried to diagnose failed embedding runs.
| Risk | Control in the Supabase pattern | Decision to make |
| Changed source columns do not refresh vectors | Insert trigger plus update trigger on title and content | Define exactly which columns affect embedding input. |
| Unrelated updates create unnecessary jobs | Column-scoped update trigger | Do not fire embedding work for fields outside the content function. |
| Provider or worker failure | pgmq visibility timeout retry behaviour | Set a timeout long enough for normal processing but short enough for useful retry. |
| Old embedding served after content update | Optional before-update clearing trigger | Choose between temporary stale search and temporarily missing search. |
| Dimension mismatch | Model and column dimension check before worker rollout | Keep text-embedding-3-small aligned with halfvec(1536), or change both together. |
| Worker runs too long | Smaller batches, fewer concurrent requests and adjusted schedule | Size the worker against documented Edge Function and Cron limits. |
| Opaque failures | failedJobs, response headers and net._http_response | Verify diagnostics before adding the search UI. |
Step 5: verify the pipeline before adding search UI
Do not build retrieval prompts or a chat interface until the data pipeline proves it can keep embeddings current. For the illustrative public.documents table, verify the workflow in this order:
- Confirm the Supabase project supports
pgmq; the supplied Supabase Queues documentation states availability from Postgres version 15.6.1.143 or later.
- Enable
vector, pgmq, pg_net, pg_cron and hstore.
- Check that the embedding column matches the selected model dimensions before running the worker.
- Insert one illustrative document row and confirm the trigger creates a queue message with row id, schema, table, content function and embedding column.
- Run or wait for the scheduled processor and confirm the embedding column is populated.
- Update a non-embedding field and confirm no unnecessary embedding job is queued.
- Update
title or content and confirm a new job is queued.
- Decide whether stale embeddings are acceptable during regeneration. If not, add the optional clearing pattern described by Supabase and make search tolerate missing embeddings.
- Inspect the Edge Function response for
completedJobs, failedJobs, x-completed-jobs and x-failed-jobs.
- When failures occur, inspect
net._http_response for the asynchronous HTTP response and error detail.
- Confirm processed queue messages are deleted by the worker; otherwise, expect retries after the
pgmq visibility timeout.
Only after those checks pass should you add vector retrieval, ranking logic or RAG prompts. Supabase Automatic Embeddings gives you the consistency path, but the product quality still depends on choosing the right embedding input, handling missing or stale vectors deliberately, and observing the worker when it fails. If you are designing the retrieval endpoint next, use this alongside the Supabase pgvector RAG backend workflow rather than treating embeddings as the whole search product.
The next action is to implement the reference pipeline against one table, with one embedding model and one verification row, before generalising it across the rest of your content.