> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deepface.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer-owned face search with Supabase pgvector

> Use deepface.dev to generate face embeddings while storing and searching the face database in your own Supabase project.

# Customer-owned face search with Supabase pgvector

Use this pattern when you want scalable face search, but you do not want
deepface.dev to host your face database.

The flow is:

1. Send an image to `POST /represent`.
2. Store the returned embedding in your own Supabase `pgvector` table.
3. For a new query image, call `POST /represent` again.
4. Search your own Supabase table for the closest embeddings.

deepface.dev still processes the images and embeddings you submit so it can
return API results. The difference is that your enrolled face database, person
IDs, labels, and search index stay in your Supabase project.

## How this relates to DeepFace register and search

Open-Source DeepFace now includes database-backed `register`, `search`, and
`build_index` functions for stateless face recognition pipelines. In that model,
`register` stores embeddings in a backend database and `search` queries that
database.

deepface.dev does not expose hosted `/register` or `/search` endpoints today.
Instead, use `POST /represent` for the compute step and keep registration,
storage, indexing, and search in your own database.

References:

* [Sefik's write-up on DeepFace register, search, and build\_index](https://sefiks.com/2026/01/01/introducing-brand-new-face-recognition-in-deepface/)
* [Open-Source DeepFace database-backed search in the README](https://github.com/serengil/deepface)

## Create the Supabase table

This example uses `Facenet`, which returns 128-dimensional embeddings. If you
choose another model, confirm its embedding length and change `vector(128)` to
match.

```sql theme={null}
create extension if not exists vector;

create table if not exists public.face_embeddings (
  id uuid primary key default gen_random_uuid(),
  person_id text not null,
  label text,
  embedding vector(128) not null,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

alter table public.face_embeddings enable row level security;

create index if not exists face_embeddings_embedding_hnsw_idx
  on public.face_embeddings
  using hnsw (embedding vector_cosine_ops);
```

For production, add RLS policies that match your app's account model. If you use
a service-role key in a backend job, keep it server-side only and never expose it
in browser code.

## Query nearest faces

Use cosine distance (`<=>`) to match deepface.dev's default `cosine` comparison
metric. Smaller distances are closer matches.

Create a database function so Postgres can order by vector distance directly:

```sql theme={null}
create or replace function public.match_faces(
  query_embedding vector(128),
  match_count int default 5
)
returns table (
  id uuid,
  person_id text,
  label text,
  metadata jsonb,
  distance float
)
language sql
stable
as $$
  select
    face_embeddings.id,
    face_embeddings.person_id,
    face_embeddings.label,
    face_embeddings.metadata,
    face_embeddings.embedding <=> query_embedding as distance
  from public.face_embeddings
  order by face_embeddings.embedding <=> query_embedding
  limit match_count;
$$;
```

## JavaScript example

Install the Supabase client in your backend project:

```bash theme={null}
npm install @supabase/supabase-js
```

Then generate, store, and search embeddings from a server-side script or API
route:

```javascript theme={null}
import { createClient } from "@supabase/supabase-js";

const deepfaceApiBaseUrl =
  process.env.DEEPFACE_API_BASE_URL || "https://api.deepface.dev";
const deepfaceApiKey = process.env.DEEPFACE_API_KEY;
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!deepfaceApiKey || !supabaseUrl || !supabaseServiceRoleKey) {
  throw new Error("Missing DEEPFACE_API_KEY, SUPABASE_URL, or SUPABASE_SERVICE_ROLE_KEY.");
}

const supabase = createClient(supabaseUrl, supabaseServiceRoleKey, {
  auth: { persistSession: false },
});

async function represent(imageBase64) {
  const response = await fetch(`${deepfaceApiBaseUrl}/represent`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${deepfaceApiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model_name: "Facenet",
      img: imageBase64,
    }),
  });

  if (!response.ok) {
    throw new Error(`deepface.dev /represent failed: ${response.status}`);
  }

  const result = await response.json();
  const embedding = Array.isArray(result) ? result[0]?.embedding : result.embedding;

  if (!Array.isArray(embedding)) {
    throw new Error("No embedding returned by deepface.dev.");
  }

  return embedding;
}

async function registerFace({ personId, label, imageBase64, metadata = {} }) {
  const embedding = await represent(imageBase64);

  const { error } = await supabase.from("face_embeddings").insert({
    person_id: personId,
    label,
    embedding,
    metadata,
  });

  if (error) {
    throw error;
  }
}

async function searchFace({ imageBase64, limit = 5 }) {
  const embedding = await represent(imageBase64);

  const { data, error } = await supabase.rpc("match_faces", {
    query_embedding: embedding,
    match_count: limit,
  });

  if (error) {
    throw error;
  }

  return data;
}
```

## When to use this pattern

Use customer-owned search when you need:

* face search across many enrolled identities;
* control over where biometric data and metadata are stored;
* your own RLS, audit, retention, and deletion rules;
* database-level ANN search as your dataset grows.

For small request-time comparisons, `POST /compare` can still compare one image
or vector against vectors you include in the request. For larger datasets, keep
the database search in Supabase and use deepface.dev only for embedding
generation.
