#Supabase
If your users already sign in with Supabase Auth, there is nothing to sign and no code to write. Name the project and Vatio verifies the Supabase session itself, on the web, on WhatsApp and on Instagram.
vatio auth --supabase
That finds the project URL where your app already keeps it (a .env, or the
src/integrations/supabase/client.ts Lovable generates), checks that it signs
with keys Vatio can verify, and writes:
# vatio.yml
auth:
supabase: https://abcdefghijklmnopqrst.supabase.co
It also stores the project URL and its anon key as the secrets
SUPABASE_URL and SUPABASE_ANON_KEY, for your tools. Pass the URL
yourself if it cannot find it: vatio auth --supabase https://<ref>.supabase.co.
#Or connect it from the inbox
Under Channels → Supabase in the inbox, Connect Supabase signs in to Supabase
and asks you to pick the project (none to pick if the organization has just
one). Vatio sets SUPABASE_URL and SUPABASE_ANON_KEY from it, and shows the
auth: block to add to vatio.yml. From then on vatio auth --supabase uses
that project instead of looking in your .env.
The connection reads the organization's projects, public API keys and auth
settings, never your data, and never stores a secret or service-role key.
Which project verifies visitors is still the one vatio.yml names, so
preview can point at a staging project and a rollback brings back the project
it deployed with. vatio diff warns when the two disagree. Disconnecting revokes
Vatio's access and leaves the secrets and vatio.yml as they are.
Vatio still only verifies: it reads the project's public keys from
/auth/v1/.well-known/jwks.json, which can check a token but never create
one. It never sees a refresh token.
#Before you start: signing keys
Vatio needs your project to sign with an asymmetric key (ES256 or RS256). A project still on the legacy JWT secret (HS256) publishes no public keys, and the push is refused with this fix:
- In the Supabase dashboard, open Project Settings → JWT Keys.
- Migrate the JWT secret, then rotate to the ES256 key it creates.
Your app keeps working through the switch; supabase-js does not change.
Custom domains are not supported yet: use the https://<ref>.supabase.co URL.
#On the web
Put the widget on your site as usual. That is all. The widget reads the
Supabase session your page already has, where supabase-js keeps it
(localStorage, or the cookies @supabase/ssr writes):
- Signed in: the visitor is known from their first message.
- Signs in mid-conversation: the same conversation carries on, now with a name on it (Signing in mid-conversation).
- Signs out, or someone else signs in: a new conversation. One person never sees another's.
- Token refreshed: supabase-js replaces the access token every hour and the widget picks up each new one, so a long conversation keeps working.
To decide yourself, pass the token and the widget stops reading the session:
window.VatioWidget.identify(session.access_token); // null to sign out
That is also how the mobile SDKs and @vatio-ai/sdk do it: pass
session.access_token as the visitor token.
#On WhatsApp and Instagram
Add your login page as sign_in_url, and keep the widget script on it:
vatio auth --supabase --sign-in-url https://your-app.com/login
When a private tool needs a signed-in contact, Vatio sends a Sign in
button. It opens your login page with ?vatio_sign_in=<code>, and the widget
takes it from there:
- Already signed in on that phone? The widget hands the session to Vatio at once. Nothing to type.
- Not signed in? They sign in the way your site already works (password, magic link, Google). Your redirects can go anywhere on your site with the widget on it; the code is kept for the tab for 10 minutes.
- Vatio verifies the token. On WhatsApp, when the account's
phonematches the WhatsApp number, that is it; otherwise the person confirms the number (•••• 5678) or the Instagram handle, so a forwarded link cannot connect someone else's account. - The conversation picks up where it left off, and the agent runs the tool it was waiting on.
A Supabase access token lasts an hour by default, so the contact is asked to sign in again in a later conversation. On a phone that is already signed in it is one tap. To make it rarer, raise Access token expiry under Project Settings → JWT Keys.
#Tools
Mark a tool access: private and forward the token. Supabase's REST API
answers as the visitor, so your Row Level Security decides what they see,
exactly as in your app:
# tools/my_children.yml
description: The visitor's children and their next appointments.
when_to_use: They ask about their own children or bookings.
access: private
request:
method: GET
base_url: $env.SUPABASE_URL
path: /rest/v1/children
query:
select: "name,next_appointment"
headers:
apikey: $env.SUPABASE_ANON_KEY
Authorization: Bearer $auth.token
respond:
data:
children: "$"
message:
when_empty: They have no children registered.
default: "Found {{count}} children."
A database function works the same way, as POST /rest/v1/rpc/<name>, and so
does an Edge Function, as POST /functions/v1/<name>.
Never give a tool the service-role key: it reads past every policy. With the visitor's own token, the model cannot ask for anyone else's rows, because the database will not return them.
#What a tool sees
| Placeholder | Value |
|---|---|
$auth.subject | The Supabase user id (auth.uid()) |
$auth.token | The access token, to forward |
$auth.claims.email, $auth.claims.phone | From the token |
$auth.claims.user_metadata, $auth.claims.app_metadata | From the token |
The contact in the inbox gets the email, the phone, and the name from
user_metadata.full_name (or name). To show a profile link and badges in the
inbox, add a metadata claim with a
Custom Access Token Hook;
see Enrich the inbox.