Authentication Model — Auth-First for All
Onoots is an authenticated-from-the-start app. There is no anonymous, identity-less visitor: the moment someone lands on onoots.com, the app opens a session for them. Every interaction with the platform — chatting with an agent, sending a lead, saving a dream, requesting a showing, joining a call — is tied to a real identity.
There is no login wall. The session is created transparently in the background. Visitors browse the public catalog exactly as before — they just also carry a real session from their first request.
Transparent anonymous sessions
The enabler is Supabase Anonymous Sign-Ins. On a real browser’s first navigation, the middleware mints an anonymous session:
visitor → middleware → no session?
→ signInAnonymously() → real auth.users row (is_anonymous = true) + JWT- The session is a real
auth.usersrow withis_anonymous = trueand a normal JWT — not a fake/guest token. Soauth.uid()is available everywhere (RLS, API routes, calls). - A
profilesrow is auto-created by thehandle_new_usertrigger (it tolerates the null email/metadata of an anonymous user). - Crawlers are excluded. Googlebot/bingbot/social/AI bots (by User-Agent) never get a session minted — the public catalog stays server-rendered for
anon, fully indexable, andauth.usersisn’t inflated by crawl traffic. - Graceful degradation. If anonymous sign-ins are ever unavailable, the middleware sets a short-lived
onoots-anon-offcookie and the catalog keeps serving — nothing breaks.
Bound identity
Because every visitor has a uid, everything they create is attributable:
| Interaction | Bound via |
|---|---|
| Lead (chat / form / showing request) | leads.user_id = auth.uid() |
| Dream | dreams.user_id = auth.uid() |
| Buyer conversation | conversation_participants client keyed by auth.uid() |
The buyer being a participant keyed by auth.uid() is what lets them read their own thread via RLS and start LiveKit voice/video calls with their real session — no opaque bearer token.
Anti-abuse is unchanged. An anonymous session does not stop bots, so Cloudflare Turnstile + per-IP rate limits still guard every public write.
Upgrade to a permanent account
When a visitor registers, the app upgrades the existing anonymous session in place instead of creating a brand-new user:
anonymous session → updateUser({ email, password }) → same uid, now permanentThe uid is preserved, so all of their history carries over — leads, dreams, and conversations stay attached. Registration is a promotion, not a fresh start.
Gated areas
The public catalog is open to everyone (anonymous included), but operator surfaces require a permanent account:
/dashboardand/crmredirect to/auth/loginwhen the session is missing or anonymous.- Agent-only APIs reject anyone without a row in
agents— an anonymous visitor never qualifies.
Housekeeping
Most transparent sessions never engage. A daily anonymous sweep cron (/api/cron/anon-sweep) hard-deletes anonymous, email-less users older than 30 days that have no lead, dream, or conversation — keeping auth.users bounded. Engaged or upgraded users are structurally excluded.
Why
This removed the ill-defined “anonymous buyer”: previously the buyer chat ran without a session (service-role + an opaque conversation bearer), which blocked authenticated features like calls. With auth-first, the buyer is always a real, identifiable participant — and the communication component (voice/video/call) builds directly on top.