Skip to main content

Customer-owned face search with Supabase pgvector

Use POST /represent for embedding computation and keep your enrolled database in your own Supabase project. deepface.dev does not expose hosted /register or /search endpoints. Images and embeddings submitted for computation still leave your backend; customer-owned storage does not mean local-only processing. This worked example supports one authenticated Supabase user per tenant and pseudonymous UUID subjects within that tenant. Organization/team membership is a different authorization model and needs its own policies and isolation tests. Face similarity alone is not legal identity, liveness or complete KYC.

Prepare an isolated customer sandbox

Use Node.js 24 and install @supabase/supabase-js in your backend project. The transport fixture was tested with version 2.108.2. Save the files below with the shown directory structure; complete code is available here without private repository access. Run schema.sql once as the owner of a fresh sandbox. It intentionally fails if these example tables already exist. It is not a migration for deepface.dev or a script to apply blindly to an existing database. This schema assumes pgvector is in extensions. If an existing installation is in another schema, stop for owner review; do not relocate a shared extension blindly. The schema enables and forces RLS, revokes inherited anonymous/default grants, and uses an invoker-rights search function. authenticated can access only rows whose tenant is auth.uid(). Only the database owner provisions pipeline rows. The application uses a publishable key plus the owner’s verified user session, never a secret/service-role key. Service roles and database superusers bypass RLS; RLS is not a boundary against a compromised administrator. Supabase RLS guidance and PostgreSQL function privileges explain these boundaries.

Provision and preserve a pipeline

Before real enrollment, the owner must insert a row into face_search_pipelines for the actual authenticated user. Set a new pipeline UUID, allowed model, reviewed provider/runtime revision, detector, alignment, normalization, reviewed detector fallback policy and measured embedding dimension. Do not copy the synthetic test pipeline into production. Confirm the current model/detector allowlists through capabilities and review model rights. The /represent response identifies model_name, but does not certify an exact weight/runtime revision. provider_revision is the customer’s independently verified record; a populated string is not proof. If revision continuity cannot be established, stop production enrollment/search until the owner has a verified pipeline or has evaluated and migrated to a new one. Do not invent a revision. The model server can retry detection using a fallback detector (for example, OpenCV to RetinaFace), even when detector_backend was explicit. The response does not report the effective detector. Record the independently reviewed fallback behavior in detector_policy, evaluate that complete preprocessing policy, and bind it to provider_revision. An explicit detector parameter alone is not proof of preprocessing identity. If your job requires one detector only, production use requires an independently verified configuration without fallback. See detector behavior. Keep provisioned profiles immutable: add a new pipeline UUID when any model, revision or preprocessing setting changes. Equal dimensions do not establish compatibility. Each request uses the selected profile’s explicit settings.

Integrate with existing authorization

Set DEEPFACE_API_KEY, SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY through your server’s secret/configuration system. Obtain the user access token from the existing authenticated backend request. Never solicit tokens in chat or put them in source code, command arguments or browser bundles. Create one client per request; do not share one user’s client with another. authorize is a required trusted application function. It must check the verified actor’s current enrollment, search or deletion permission and the applicable permission/consent records for the subject and intended use. A client-supplied authorized: true flag is not this function. No consent or account interface is provided or changed by the example.
The application must supply the permission policy, authentic session, verified pipeline and permitted images before this integration can perform real work. Prepared code or synthetic tests do not establish those prerequisites.

Outcomes, retries and deletion

An enrollment reserves (tenant_id, enrollment_id) atomically before compute. Concurrent workers cannot both win the same reservation. A completed duplicate returns already_enrolled without inference. A pending reservation blocks another attempt; failures or lost responses never expire it automatically. Preserve the enrollment and compute request IDs, investigate the result, and do not change IDs or delete/recreate the subject to evade an uncertain computation. A lost database acknowledgement can follow completed inference. Repair storage from a securely retained known result when authorized; do not recompute blindly. No raw embedding recovery store or automatic retry is included in this example. One image must yield exactly one face with the provisioned model and dimensions. Multiple faces or incompatible results leave enrollment pending for review. The code omits paid positive priority and never automatically replays compute. No-face outcomes can be billable; see errors, limits and pricing. Search requires an explicit evaluated cosine-distance threshold from 0 to 2. Only complete rows from the same tenant and pipeline within that threshold are returned. An empty result is no_acceptable_match; returned candidates still require the application’s intended verification/decision process. The SQL uses exact search for a bounded worked example. It makes no latency, capacity or ANN recall claim. Set appropriate database timeouts and test the intended dataset. Before adding HNSW/IVFFlat, measure filtered recall and no-match behavior against this exact baseline. See the pgvector reference. Deleting a subject cascades to its pending and complete enrollments. It removes active rows; backups, replicas, application logs and external copies need the customer’s separate retention/deletion process. This example stores no labels, raw images or arbitrary metadata. Keep returned pseudonymous IDs private too.

Verify with synthetic fixtures

The included SQL suite uses synthetic 3-dimensional vectors, not face embeddings. Run fixture SQL only through the disposable runner, never against a shared or customer Supabase project. It checks non-owner RLS, cross-tenant read/search/write/delete denial, pipeline separation, invalid/zero/nonfinite inputs, explicit no-match, immutable completed enrollment, unique reservations and deletion. It does not validate recognition accuracy, Supabase JWT verification or a deployed Data API. With PostgreSQL tools and pgvector installed locally, run from the directory containing examples/ and scripts/:
The runner creates and stops its own temporary Unix-socket-only PostgreSQL instance and removes it afterward. It accepts no production database URL and creates no passwords. It supplies a local test implementation of auth.uid(); Supabase’s verified JWT-to-role/claim boundary still needs integration acceptance. Its optional source-build mode is for testing pgvector without a global extension installation; that mode does not verify CREATE EXTENSION deployment. For the real sandbox, also verify authenticated owner and cross-tenant requests through Supabase’s Data API, configured grants/extension schema, pipeline revision continuity, permitted fixture provenance, threshold calibration and a bounded API cost cap. These are release inputs, not successes of local fixtures.

Complete files

examples/customer-owned-search/schema.sql

examples/javascript/client.mjs

examples/customer-owned-search/store.mjs

examples/customer-owned-search/test.sql

scripts/test-customer-search-db.mjs