Auth

lib/auth (Rust kalt-auth) issues and verifies access JWTs. app/web/app/lib/auth is the browser session. svc/auth is the HTTP service behind /api/auth. There is no Supabase Auth.

Flow

  1. Type a phone number and submit.
  2. POST /api/auth/initiate returns okid_pending. The app opens /id/pending/{okid}.
  3. svc/communication subscribes to that auth message and sends an SMS with {AUTH_ORIGIN}/id/{okid_approval}.
  4. Opening that link loads /id/{okid}.
  5. Approve mints access and refresh tokens for a one-shot POST /api/auth/claim. Decline stores nothing.

The TypeScript lib persists the claimed tokens and attaches Authorization: Bearer on later API calls. Public challenge routes never send a bearer, so a leftover token cannot block sign-in through the gateway.

Tokens

Access JWT (HS256, AUTH_JWT_SECRET), about 15 minutes:

{ "sub": "user-id", "role": "editor", "phone": "+47…", "exp": 1710000900 }

Refresh token is opaque, stored hashed in Postgres, rotated on POST /api/auth/refresh. signOut() revokes it.

Frontend

import { accessToken, claimAndInstall, signOut, withAuthHeaders } from '~/lib/auth'

const user = useAuthUser()
await claimAndInstall(okid)
const token = await accessToken()
await withAuthHeaders('/api/publications')
await signOut()

useAuthUser() and useIsEditor() (user.role === 'editor') read role from the access JWT. Public /api/auth and /api/geo paths skip Bearer.