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
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 anemail 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.