Phases: public repo & Docker (7), guest-first optional accounts (8), production hardening (9), Render deploy (10). Confirmed decisions and per-phase open questions recorded in each file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
4.0 KiB
4.0 KiB
Phase 8 — Optional accounts & multi-user
Goal: turn the single-user app into a multi-user one where accounts are optional: a visitor can start playing instantly as a guest, and can register (email + password) at any point to keep their adventures across devices/browsers. This is the largest phase — it touches every router and most tables.
Decisions (confirmed)
| Question | Answer |
|---|---|
| Auth method | Email + password, optional — guest sessions work without an account |
| Signup policy | Open signup (rate-limited) |
| LLM API keys | BYOK + shared demo key — users can paste their own key; users without one get limited turns on a server-funded key |
Ask before implementing:
- Demo-key specifics: which provider/key funds it, model whitelist (free models only?), per-user turn/day cap, what the "out of demo turns" message says.
- Guest data retention: how long before unclaimed guest data is deleted (suggestion: 30 days of inactivity).
- Password reset: skip for v1, or implement email-based reset (requires an email provider)?
Data model
Usertable: id, email (nullable — null means guest), password_hash (nullable), created_at, last_seen_at, is_guest flag (derivable from email; keep explicit for clarity).- Add
user_idFK to:Adventure,Scenario,Script,Settings(and anything else global today — auditmodels.py). Story cards/actions inherit scope via their parent. - Settings becomes per-user (endpoint URL, API key, models, memory-bank config).
API key encrypted at rest (Fernet with a server-side
SECRET_KEYenv var). - Migration (in
migrations.pystyle): create a "local user", assign all existing rows to it — a fresh clone/local install keeps working exactly as before. - Demo/starter scenarios: mark as
user_id = NULL+is_publicso everyone sees them (decide exact mechanism when implementing; seed viaseed_demo.py).
Auth & sessions
- Guest flow: first API call with no session → create guest User, set a signed, long-lived httpOnly session cookie. No signup wall anywhere.
- Register: email + password (hashed with bcrypt/argon2) upgrades the current guest user in place — same user_id, data automatically kept.
- Login: standard session issue; logging in from a fresh guest session with existing account discards the empty guest (or merges — ask if guest has data).
- Session middleware/dependency: every router handler resolves
current_user; every query filtered byuser_id(this is the bulk of the diff — go router by router). - Rate limits on register/login endpoints (brute-force protection).
- Local/self-hosted mode stays frictionless: single auto-created local user, no login UI
unless
MULTI_USER=true(env var) — resume demo runs multi-user, local clones don't care.
Shared demo key (BYOK fallback)
- Server env vars:
DEMO_API_KEY,DEMO_ENDPOINT_URL,DEMO_MODEL_WHITELIST,DEMO_TURNS_PER_DAY. - If a user has no API key configured: use demo key, restrict model picker to the whitelist, count turns per user per day, friendly error + "add your own key in Settings" when capped.
- Turn counting includes memory-bank background calls (or disable memory bank on demo key — decide when implementing).
Frontend
- Auth UI: register/login modal or page, "Save your progress" nudge for guests (subtle, e.g. in the header), logout, account menu.
api.js: send cookies (credentials: include), handle 401 → re-establish guest session.- Settings page: per-user; show demo-key status ("Using shared demo key — N turns left today").
Exit criteria
Two different browsers hit the deployed app: each gets its own guest world (adventures invisible
to the other), both can play immediately on the demo key. One registers mid-adventure and its
data survives; logging in from the other browser shows the same account data. Local
start.ps1 / docker compose up still works with zero auth friction.