SupabaseAuth — identity + account pairing (Phase 2)
Status: Fundament built + on master (schema live, slim PWA client, migrations CI). PWA login gate + account pairing + extension sign-in + relay sync are planned (see Plans below). This doc is the native-client contract for the new auth layer.
1. Role — what Supabase is (and isn’t) in VSRelay
Supabase provides identity + metadata only. It does not carry agent traffic.
Client (PWA / native) Supabase Extension (Cursor/VS Code)
──────────────────── ───────── ──────────────────────────
OAuth login ───────────► auth.users (GitHub/Google)
read my rooms (RLS) ─────► rooms / devices (RLS) ◄───── registers its room
│ pick a room (extension sign-in)
▼ relay_client_auth{roomId, deviceToken}
─────────────────────── Node relay (TRANSPORT, unchanged) ──────────────►
(relay validates room/device against Supabase; stays a transparent forwarder)- Supabase = who you are (OAuth identity) + which editors/devices you own (rooms/devices).
- Relay = the transport. Unchanged, still a transparent forwarder. It does not speak Supabase auth to the client; it validates room/device credentials that originate from Supabase (via a server-side sync — “Option A”).
- Extension = registers its room under your account so your phone can find it.
The agent-control protocol (prompts, sessions, terminals, files, diagnostics, recovery) is unchanged — Supabase sits in front of it as the identity gate + the pairing directory.
2. Auth model
- OAuth only — GitHub + Google. No email/password, no magic links, no anonymous sign-in.
- PKCE flow for every client (web SPA + native).
- Session = a Supabase JWT (
access_token+refresh_token), auto-refreshed.auth.uid()(the user id) is the identity used by all Row-Level-Security policies. - OAuth runs in the system browser — never an embedded webview (App Store requirement + security). Web: a redirect. Native:
ASWebAuthenticationSession(iOS) / Custom Tab (Android) → a deep link. - Session storage: web = Supabase-managed
localStorage; native = OS keychain (Keychain / Keystore), never plain prefs.
3. Schema + RLS (the tables a client touches)
All in public, all RLS owner-scoped (TO authenticated, (select auth.uid()) = user_id; UPDATE has USING + WITH CHECK). Tokens are stored as hashes, never raw.
| Table | Purpose | Client reads |
|---|---|---|
profiles | mirror of auth.users (github_username, display_name, avatar_url); auto-created by a trigger on signup | your own profile (for the header chip) |
rooms | a registered editor instance — room_id (matches the relay’s room), relay_url, extension_token_hash, label, last_seen_at | your rooms = the pairing list |
devices | a paired phone — room_id, device_token_hash, platform, label, last_seen_at | your devices |
A client only ever sees its own rows (RLS). The Data API is reached with the publishable key as apikey + the user’s access_token as the bearer (so RLS scopes to that user).
4. How a client authenticates
Web (PWA — reference implementation):
auth.signInWithOAuth({ provider: 'github' | 'google', options: { redirectTo: window.location.origin } })→ system browser.- Provider → Supabase callback → back to the app origin with a
?code=. - The client (with
detectSessionInUrl) exchanges the code → session persisted;onAuthStateChangefires. - The client points its DB reads at the user’s token (
setDbAuth(access_token)) so RLS works.
Native (iOS/Android):
signInWithOAuth({ provider, redirectTo: 'vsrelay://auth-callback' })opened inASWebAuthenticationSession.- Provider → Supabase callback → deep link
vsrelay://auth-callback?code=…. - App captures the deep link →
exchangeCodeForSession(code)→ session stored in the keychain by the SDK.
vsrelay://auth-callback, https://app.vsrelay.dev/**, https://vsrelay.dev/**, and http://localhost:5173/** are in Supabase’s redirect allow-list. (The PWA is served at https://app.vsrelay.dev; the vsrelay.dev entry is retained as a transition-period safety net.)
5. Account-based pairing (replaces manual token paste)
After login, the client reads its rooms (RLS-scoped — no server code needed):
GET {SUPABASE_URL}/rest/v1/rooms?select=* (apikey: publishable, Authorization: Bearer <access_token>)
→ [{ room_id, relay_url, label, last_seen_at, ... }]The user taps a room → the client connects to the relay exactly as today:
wss://<relay_url> → { type: 'relay_client_auth', roomId, deviceToken, ... }deviceToken is issued (once) when the device is paired and stored client-side (keychain on native). No copy/paste — the phone signs in and sees the user’s editors. (The extension populates rooms by signing in itself; the relay validates the roomId/deviceToken against Supabase via a server-side sync.)
6. Relationship to the relay (doctrine)
- The relay stays a transparent forwarder — it never inspects payloads. Validating a
roomId/deviceTokenagainst Supabase is admission/routing, not payload inspection → compliant. - For v1 the relay’s room/device list is synced from Supabase (Option A — zero relay protocol change). A live Supabase query on the relay (Option B) is a later optimization.
- E2E payload encryption is a separate future phase (Phase C) — independent of this identity layer.
7. Doctrine for native clients
- Thin renderer. Supabase owns the auth flow; the client opens the system browser, stores the session, reads its rooms, renders a list. No auth logic, no patch logic.
- System browser only for OAuth (
ASWebAuthenticationSession, notWKWebView). - Keychain for the session (never
UserDefaults/plist). - Publishable key only client-side. The
service_role/secret key never ships in a client. - Open string unions for any auth/pairing
errorCodethe client renders — unknown values degrade to a neutral message. - New providers / pairing changes ship server-side (Supabase + relay) — no native release required.
8. Project values (public)
| Project URL | https://smafvhchpoumnpobqenq.supabase.co |
| Publishable key (safe to ship in clients) | sb_publishable_CEHMCHr82txYNktovte7DA_kgmZa-qr |
| OAuth callback | https://smafvhchpoumnpobqenq.supabase.co/auth/v1/callback (optionally https://auth.vsrelay.dev/auth/v1/callback once the custom domain is set) |
| App origin (prod) | https://app.vsrelay.dev (PWA, live on Vercel; apex vsrelay.dev serves the marketing site) |
9. Built vs planned
- ✅ Built (master): schema
profiles/rooms/devices+ RLS +handle_new_user; slim PWA client (@supabase/auth-js+@supabase/postgrest-js, exportsauth/db/setDbAuth); migrations CI; GitHub + Google providers live. - 🔨 Planned: PWA login gate (
AuthGate+LoginScreen) → account pairing (PairingScreen+pairWithRoom) → extensionvsrelay.signIn(registers its room) → relay Option-A sync.
References
- Spec:
docs/superpowers/specs/2026-05-24-vsrelay-phase2-supabase-auth-design(source repo) - Plans:
docs/superpowers/plans/2026-05-24-vsrelay-phase2-fundament,…-pwa-oauth-login(source repo) - Reference renderer:
packages/web/src/core/supabase.ts,components/AuthGate.tsx,components/LoginScreen.tsx - Relay contract:
Relay· Trust tiers:ProductionDeploymentArchitecture