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!,
});| Option | Type | Description |
|---|---|---|
supabaseUrl | string | Your Supabase project URL. |
supabaseAnonKey | string | Your Supabase anon key — the public one. Never the service-role key. |
The return value:
| Field | Type | Description |
|---|---|---|
supabase | SupabaseClient | A standard Supabase client. Use it exactly as you always would. |
adapter | FaceGateSupabaseAdapter | The 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.
- 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. - The server verifies the face. On sign-in, the FaceGate server matches the face and runs liveness.
- 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_tokenandrefresh_token. - Those tokens come back in
provider_session. The verification response carries them infaceGateSession.provider_session. - The adapter calls
setSession.setSessionFromFaceGatereads the tokens and callssupabase.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.
| Condition | Message | Cause |
|---|---|---|
provider_session is null | No 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 missing | FaceGate 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 tokens | Failed 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.