Files
famdone/auth-v2.md
T
2026-08-14 18:50:37 +01:00

9.5 KiB

Auth architecture

Current implementation: SvelteKit directly + PocketBase. Hono is not part of auth today — it is reserved for future services (email, etc.). The /api proxy target still points at it, but no auth traffic flows through it.

                        ┌────────────────────────────────────────────────────────┐
                        │                      BROWSER (Svelte)                  │
                        │   /login /signup forms · $app/forms · use:enhance     │
                        └──────────────────────────┬─────────────────────────────┘
                                                   │ 1. form action POST /login
                                                   │    /signup  (returns result)
                                                   ▼
                        ┌────────────────────────────────────────────────────────┐
                        │                SVELTEKIT SERVER (Node)                 │
                        │                                                        │
                        │  hooks.server.ts  (runs once per request, first)      │
                        │    · read httpOnly cookie  pb_session                 │
                        │    · createPbClient(token) → pb.authRefresh()         │
                        │    · → { id, username, role, famId }                  │
                        │    · event.locals.user / event.locals.pbToken         │
                        │    · route guards: public vs authed vs /admin         │
                        │                                                        │
                        │  +page.server.ts (actions / loads)                    │
                        │    login.ts / signup.ts / session.ts                  │
                        │    · createPbClient / createSuperPbClient             │
                        │      (ABSOLUTE PB URL - server-to-server, no proxy)   │
                        └──────────────────────────┬─────────────────────────────┘
                                                   │ 2. PB API (REST)            │
                                                   ▼
                        ┌────────────────────────────────────────────────────────┐
                        │              POCKETBASE (100.103.22.104:8090)         │
                        │  collections: users (auth) · fams · members · ...    │
                        │                                                        │
                        │  PB is the SOURCE OF TRUTH:                           │
                        │  · password hashing (bcrypt-style)                    │
                        │  · JWT token issue + expiry (exp claim)               │
                        │  · authRefresh / token rotation                       │
                        │  · per-collection API rules (authz boundary)          │
                        └────────────────────────────────────────────────────────┘

  Future (NOT auth):  SvelteKit → /api proxy → Hono → email & other services

Request lifecycle (authenticated)

  1. Browser sends request; sends cookie pb_session (httpOnly, sameSite=lax, secure in prod).
  2. hooks.server.ts extracts the token.
  3. createPbClient(token) builds a PB client pre-authenticated as that user.
  4. pb.collection('users').authRefresh():
    • validates the token (PB JWTs can't be checked offline),
    • returns the fresh record → event.locals.user = { id, username, role, famId },
    • returns a fresh token; if it changed, the cookie is rolled forward.
  5. Route guard runs (public / authed / admin-only).
  6. +page.server.ts / +server.ts use locals.user for identity; use the token-backed PB client for any CRUD so PB's collection rules apply.

Key decisions to replicate in another project

  • Server-only PB client lives in src/lib/server/; never imported by browser code. Secrets (superuser creds) stay server-side.
  • Absolute PB base URL in the SDK (e.g. http://host:8090), NOT a relative /pb. All calls run in Node where relative URLs fail. The Vite /pb proxy is a browser-only convenience and is unnecessary for server-side calls.
  • Env vars: read via $app/env/private for private vars (declared in src/env.ts), never $app/env/public.
  • createSuperPbClient = anonymous client + _superusers auth, used for admin-style creates (signup). Superusers bypass PB collection rules.
  • Session cookie: PB JWT in pb_session; expiry governed by the token's exp, cookie maxAge is just a ceiling; authRefresh rolls it forward.
  • Authz boundary lives in PocketBase collection rules (famId scoping), not just app code.

Signup flow (/signup)

Multi-step form on a single route. All steps run as form actions on the server; the use:enhance handler only advances the step on a non-failure result.

   ADMIN USER
   +-----------------------------------------+       1. POST ?/signup  {familyName,email,password}
   | +-----------+  +-----------+  +--------+ |  ─────────────────────────────►
   | |  step 1   |  |  step 2   |  | step 3 | |   (use:enhance advances step on success)
   | | family+   |  | username  |  | child  | |
   | | email+pass|  |           |  |  name  | |
   | +-----------+  +-----------+  +--------+ |
   +-----------------------------------------+
                                        ▲ 3. set httpOnly cookie pb_session
                                        │    return {success:true}
                                        ▼
   ┌────────────────────────────────────────────────────────────────────────┐
   │  /signup/+page.server.ts  actions: signup · username · child          │
   └────────────────────────────────────────────────────────────────────────┘
                                    │
              ?/signup              │              ?/username         ?/child
              ─────────────────────┼───────────────────┬───────────────────────
              signupAdmin()        │   setUsername()   │   createChild()
              ─────────────────────┼───────────────────┴───────────────────────
              │                    ▼                     (famId from locals.user)
              ▼
   createSuperPbClient()  ── superuser-authenticated PB client
              │
              ├─(1) fams.create({ name, slug })            → family.id
              ├─(2) users.create({ email,password,name,    → user, role not set
              │        famId: family.id })
              ├─(3) users.authWithPassword(email,password) → token
              └─(4) setSessionCookie(cookies, token)
                          │
                          ▼
              hooks.server.ts picks up the cookie next request
              → authRefresh → event.locals.user populated → user is "logged in"

   Failure path:  any PB error → throw SignupError(msg)
                  → action returns fail(status, { message })
                  → FE renders form.message, does NOT advance step

Signup step detail

Step Action Server fn Writes Returns
1 ?/signup signupAdmin fams + users cookie set, {success:true}
2 ?/username setUsername users.name {success:true, username}
3 ?/child createChild members (name, famId) {success:true, code}

Notes / caveats in the current code

  • users collection has no role/username fields yet — setUsername writes to name, and role isn't persisted (hooks reads it as undefined). Add role + username to users if you need role-based guards (/admin relies on role==='admin').
  • createChild writes a members record with inviteCode, but members has no inviteCode field (it's dropped). The member is created without a users auth record, so it can't log in yet — see "Extending — Child / device accounts (Option C)" in the root readme.md.
  • fams.createRule is null (admin-only), users.createRule is "" (public); signup uses a superuser client, so both work regardless.