Skip to main content
Supabase Auth is your source of truth. UserKit needs to know which of your users is on the page, and it will not take the page’s word for it — your server signs the claim.
The identity secret never enters the bundle. Never prefix it with NEXT_PUBLIC_, never render it into HTML, never send it to the browser to save a round trip. Whoever holds it can mint a verified session for any of your users.
The contract is the one on the federated identity page and nothing here changes it:

What Supabase supplies

One value: user.id, the uuid on auth.users. That is the external_id.
lib/supabase.ts
getUser() and not getSession(). getSession() decodes the cookie the browser sent, so signing its contents would mean signing a value the browser chose. getUser() asks Supabase.

The server half

app/api/userkit-boot/route.ts
Signed out answers 401 rather than an empty pair: “no user” and “a user whose hash I could not compute” are different situations, and the caller’s next move differs.

The client half

getState().verified is true only when the HMAC checked out. Signed in to Supabase with verified: false means either the secret belongs to the other environment or the message signed was not exactly the external_id.

Configuration

What does not travel

A Supabase user carries an email and an email_confirmed_at. Neither becomes an identity in UserKit: an email sent through /v1/boot is stored as an attribute, does not resolve to an existing contact and does not become an identity edge. The HMAC proves the external_id and only the external_id. To prove an address, send a magic link or an email code — the two flows that arrive in it.

The full example

A runnable Next app with these files, an .env.example and the pieces this page leaves out.