Back to Docs

Architecture

Auth Architecture

How authentication works across mobile, merchant portal, and admin dashboard.

Last updated March 19, 2026

Auth Architecture

CafeRadar uses three separate auth systems optimized for different contexts.

Mobile App -- Clerk

Provider: Clerk (React Native SDK) Token format: RS256 JWT Header: X-Clerk-Token

Clerk handles sign-up, sign-in, social auth (Google, Apple), and session management. The JWT 'sub' claim contains the Clerk user ID (e.g., user_2abc123).

RLS Integration

All RLS policies use (select auth.jwt()->>'sub') to get the current user ID. This works because Supabase's PostgREST passes the JWT claims to PostgreSQL.

Important: Never use auth.uid()::text with Clerk JWTs. That function returns the Supabase auth user ID, which is NULL for Clerk-authenticated requests.

Merchant Portal -- Clerk + Email OTP

Merchants authenticate via Clerk (same as mobile), then complete email OTP 2FA.

Flow:

  1. Clerk sign-in (JWT issued)
  2. API sends OTP to merchant's business email
  3. Merchant enters OTP code
  4. HMAC-signed cookie set (24h expiry)
  5. Subsequent requests verified via cookie + Clerk JWT

Tables: merchant_otp_codes (code, merchant_id, expires_at, used)

Admin Dashboard -- PBKDF2 Sessions

Sessions: In-memory store, 24-hour expiry Cookie: admin_session (HttpOnly, Secure, SameSite=Strict)

The admin uses PBKDF2-SHA512 with 100k iterations and 32-byte salt for password hashing.

Edge Function Auth

Edge functions verify Clerk JWTs using the JWKS endpoint at /.well-known/jwks.json. For cron-triggered functions, the invoke_edge_function() SQL helper sends the service role key as Bearer token.

The shared _shared/auth.ts module provides:

  • getAuthenticatedUserId(req) -- Extract and verify Clerk user ID
  • requireAuth(req, bodyUserId) -- Verify JWT + body match (defense-in-depth)
  • timingSafeEqual(a, b) -- Constant-time string comparison