FaceGateBack to home

Supabase Adapter

@facegate/supabase turns a successful FaceGate verification into a real Supabase session. Your Row Level Security policies, your existing queries, and supabase.auth.getUser() all keep working unchanged. This is Tier 2 (Adapt) — FaceGate handles the face, Supabase stays your auth system.

The Quickstart shows the three-line version. This page explains the mechanism: where the session tokens come from, what the adapter does with them, and why there is no service-role key anywhere in your app.

Install

npm install @facegate/supabase @supabase/supabase-js

@supabase/supabase-js is a peer dependency — the adapter sets sessions on a client you create, so you need the Supabase SDK alongside it.

Create the client

createFaceGateSupabaseClient builds a Supabase client and a FaceGate adapter wired to it, and returns both.

import { createFaceGateSupabaseClient } from '@facegate/supabase';
 
const { supabase, adapter } = createFaceGateSupabaseClient({
  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
  supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
});
OptionTypeDescription
supabaseUrlstringYour Supabase project URL.
supabaseAnonKeystringYour Supabase anon key — the public one. Never the service-role key.

The return value:

FieldTypeDescription
supabaseSupabaseClientA standard Supabase client. Use it exactly as you always would.
adapterFaceGateSupabaseAdapterThe adapter that sets sessions onto that client.

The supabase instance is the same @supabase/supabase-js client you already know. The adapter is the only new piece — and it has exactly one method.

Set the session

Inside your FaceGate onAuthenticated callback, hand the session to the adapter.

// inside onAuthenticated(session):
await adapter.setSessionFromFaceGate(session);
 
// supabase is now authenticated:
const { data } = await supabase.from('employees').select('*');

setSessionFromFaceGate(faceGateSession) returns Promise<void>. After it resolves, supabase.auth.getUser() returns the signed-in user and every RLS policy applies as if the user had logged in with a password.

How it works

There is no magic and no shortcut. The session is a genuine Supabase session — FaceGate just mints it on your behalf and the adapter installs it.

  1. Enrollment creates a shadow account. When a user enrolls, the FaceGate server creates a Supabase auth account for them (for example guard-{id}@internal.gts) with a random password the user never sees.
  2. The server verifies the face. On sign-in, the FaceGate server matches the face and runs liveness.
  3. The server mints a session via the Admin API. It uses the Supabase Admin API to sign the shadow user in, producing a real access_token and refresh_token.
  4. Those tokens come back in provider_session. The verification response carries them in faceGateSession.provider_session.
  5. The adapter calls setSession. setSessionFromFaceGate reads the tokens and calls supabase.auth.setSession({ access_token, refresh_token }).

After step 5 the client holds a normal authenticated session. RLS works. Existing queries work. No data model changes.

The Admin API call happens on the FaceGate server, never in your app. Your browser only ever holds the anon key and the resulting session tokens — exactly what a password login would give you. There is no service-role key in your client bundle and no bypass of your policies.

Error cases

setSessionFromFaceGate throws in three situations. Each maps to a misconfiguration, not a runtime edge case, so they surface during integration rather than in production.

ConditionMessageCause
provider_session is nullNo Supabase session in FaceGate response. Make sure provider is set to "supabase" in your FaceGate config.The verification returned no provider session. The FaceGate config is not set to the Supabase provider.
access_token or refresh_token missingFaceGate provider_session is missing access_token or refresh_token. Check your FaceGate server Supabase adapter configuration.A provider session came back but without both tokens. The server-side Supabase adapter is misconfigured.
Supabase rejects the tokensFailed to set Supabase session: {error.message}supabase.auth.setSession() returned an error — for example expired or mismatched tokens.

Wrap the call in try/catch if you want to surface these to the user. In a correctly configured Tier 2 setup, none of them fire.

Full example

@facegate/react handles the camera and liveness; @facegate/supabase mints the session. Together they are the complete client integration.

import { FaceGate } from '@facegate/react';
import { createFaceGateSupabaseClient } from '@facegate/supabase';
 
const { supabase, adapter } = createFaceGateSupabaseClient({
  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
  supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
});
 
export function SignIn() {
  return (
    <FaceGate
      apiKey="fg_test_your_key_here"
      mode="verify"
      onAuthenticated={async (session) => {
        try {
          await adapter.setSessionFromFaceGate(session);
        } catch (err) {
          console.error('Could not mint Supabase session', err);
          return;
        }
 
        // The Supabase client is now authenticated.
        // RLS applies. Existing queries work unchanged.
        const { data, error } = await supabase
          .from('employees')
          .select('*');
 
        console.log('Signed in as', session.user.name, data, error);
      }}
    />
  );
}

The session passed to onAuthenticated is a FaceGateSession. Its provider_session carries the Supabase tokens; the adapter reads them so you never touch them directly.

Next steps

  • React SDK — every <FaceGate /> prop, enrollment mode, and theming.
  • REST API — call the verification endpoints directly if you are not using the React component.