diff --git a/.planning/phases/02-users-authentication/02-CONTEXT.md b/.planning/phases/02-users-authentication/02-CONTEXT.md
new file mode 100644
index 0000000..c570515
--- /dev/null
+++ b/.planning/phases/02-users-authentication/02-CONTEXT.md
@@ -0,0 +1,114 @@
+# Phase 2: Users & Authentication - Context
+
+**Gathered:** 2026-05-22
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Ship a complete authentication and user management system: registration (with Argon2 + breach check), JWT session management (access token in Pinia memory, refresh in httpOnly cookie), TOTP 2FA enrollment and verification, backup code issuance and invalidation, password reset via email (SMTP/Celery), sign-out-all-devices, and an admin panel (user create/deactivate/reset, quota adjustment, AI provider assignment).
+
+The existing `/api/documents`, `/api/topics`, and `/api/settings` endpoints remain public in Phase 2 — they gain auth guards in Phase 3 when per-user isolation is enforced. Phase 2 adds only the new `/api/auth/*` and `/api/admin/*` endpoints. The `documents.user_id` nullable D-03 constraint is NOT lifted in Phase 2 (that is Phase 3's migration).
+
+The frontend receives a full auth wall: Vue Router guards redirect unauthenticated users to `/login`. A new `/admin` route surfaces the admin panel, visible in the sidebar only for `role === 'admin'` users.
+
+
+
+
+## Implementation Decisions
+
+### Email Transport
+- **D-01:** Email sent via SMTP configured through env vars: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`. Add to `.env.example` with placeholder values and comments.
+- **D-02:** When `SMTP_HOST` is not set (dev/local), the password reset token/link is logged to backend stdout (visible in `docker compose logs`). The API response is still 202 — no token in response body.
+- **D-03:** Reset email dispatch is a Celery task (async). The `/api/auth/password-reset` endpoint enqueues the task and returns 202 immediately. Celery + Redis are already wired from Phase 1 (D-08/D-09).
+
+### Admin Bootstrap
+- **D-04:** First admin account is seeded on startup via `ADMIN_EMAIL` + `ADMIN_PASSWORD` env vars. On app startup (lifespan), if no users exist and both vars are set, create the admin account with `role = 'admin'`. Idempotent — skipped if any users already exist.
+- **D-05:** If `ADMIN_EMAIL`/`ADMIN_PASSWORD` are not set, log a `WARNING` at startup but do not fail. App starts normally.
+- **D-06:** Admin bootstrap creates a quota row with the standard 100 MB default (same as regular users). Consistent with the quota model — every `users` row has a `quotas` row.
+
+### API Auth Scope
+- **D-07:** Phase 2 ships only the new auth and admin endpoints. Existing `/api/documents`, `/api/topics`, `/api/settings` stay public — they gain `get_current_user` guards in Phase 3 when per-user document isolation is enforced.
+- **D-08:** Admin endpoints live at `/api/admin/*`. Every handler in this router requires `get_current_admin` (not just `get_current_user`). This enforces SEC-07: admin role verified on every admin request.
+- **D-09:** CORS locked down via `CORS_ORIGINS` env var (comma-separated list). Pydantic Settings parses it as `list[str]`. Default when not set: `["http://localhost:5173"]`. `allow_origins=["*"]` removed in Phase 2.
+
+### Frontend Auth UX
+- **D-10:** Full auth wall. Vue Router `beforeEach` guard: if no `accessToken` in `useAuthStore` and route is not `/login` or `/register`, redirect to `/login`. After login, redirect back to the originally requested route.
+- **D-11:** `useAuthStore` holds `{ accessToken, user }` in memory (never localStorage). A centralized fetch wrapper (updating `frontend/src/api/client.js`) adds `Authorization: Bearer` header and handles 401 by calling `/api/auth/refresh` (uses httpOnly cookie), retrying the original request, or redirecting to `/login` on refresh failure.
+- **D-12:** Admin panel at `/admin` route with a dedicated `AdminView` component. Sidebar link (`AppSidebar.vue`) visible only when `useAuthStore().user.role === 'admin'`. Admin panel has sub-navigation: Users, Quotas, AI Config.
+- **D-13:** Full TOTP enrollment UI in Phase 2. Enrollment screen: QR code (from provisioning URI), manual secret key display, TOTP code entry to verify enrollment. Backup codes screen: display 8–10 codes with copy-all button, explicit acknowledgment checkbox before enabling. Not deferred to Phase 3.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Requirements
+- `.planning/REQUIREMENTS.md` — AUTH-01 through AUTH-08 (full auth flow), SEC-01/02/03/05/06/07 (security cross-cuts), ADMIN-01 through ADMIN-05/ADMIN-07 (admin capabilities)
+
+### Project Decisions
+- `.planning/ROADMAP.md` — Phase 2 goal and all 5 success criteria (especially #2 TOTP flow, #4 sign-out-all + family revocation, #5 admin 403 on document access)
+- `.planning/PROJECT.md` — Key Decisions: JWT httpOnly cookie strategy, HKDF per-user key derivation (not relevant until Phase 5 but sets the precedent), admin impersonation exclusion
+- `.planning/STATE.md` — Open Questions: PyOTP `valid_window=1` recommendation for ±30s clock drift; "Audit existing codebase for any bcrypt hashes before removing passlib in Phase 2"
+
+### Phase 1 Context (carry-forward decisions)
+- `.planning/phases/01-infrastructure-foundation/01-CONTEXT.md` — D-03 (documents.user_id nullable, NOT NULL deferred to Phase 3), D-08 (Celery+Redis wired), D-09 (Redis doubles as rate-limit store), D-16 (SECRET_KEY documented in .env.example, not yet read in Phase 1 code)
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `backend/config.py` — Pydantic Settings (`SettingsConfigDict`); extend with `SECRET_KEY` (already in .env.example), `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `ADMIN_EMAIL`, `ADMIN_PASSWORD`, `CORS_ORIGINS`
+- `backend/db/models.py` — `User`, `RefreshToken`, `Quota` ORM models are fully defined and migrated; no schema changes needed for Phase 2. `AuditLog` model also ready.
+- `backend/celery_app.py` — Celery app already configured; add `tasks/email_tasks.py` for reset email dispatch (mirrors `tasks/document_tasks.py` pattern)
+- `backend/deps/db.py` — existing `get_db` dependency; add `get_current_user` and `get_current_admin` FastAPI dependencies here
+- `backend/main.py` — lifespan already handles MinIO + engine disposal; extend to add admin bootstrap and CORS update
+- `backend/ai/base.py` + `backend/ai/__init__.py` — ABC + factory pattern to mirror when structuring auth dependency chain
+
+### Established Patterns
+- **Provider pattern** (`ai/`) — dependency injection via factory; mirrors how `get_current_user` should be structured
+- **Service layer** (`services/extractor.py`, `services/classifier.py`) — pure Python modules, no FastAPI coupling; new `services/auth.py` and `services/email.py` follow the same boundary
+- **Pinia-as-Facade** — existing stores (`documents`, `topics`, `settings`) never call API directly; new `useAuthStore` follows same pattern
+- **`api/` router modules** — `api/documents.py`, `api/topics.py`, `api/settings.py`; add `api/auth.py` and `api/admin.py` following same structure
+
+### Integration Points
+- `backend/main.py` — include `api/auth.py` and `api/admin.py` routers; update `CORSMiddleware` to use `CORS_ORIGINS`; add admin bootstrap to lifespan
+- `frontend/src/router/index.js` — add navigation guard + `/login`, `/register`, `/account`, `/admin` routes
+- `frontend/src/api/client.js` — update `request()` to inject Bearer token and handle 401 auto-refresh
+- `frontend/src/stores/` — add `auth.js` (useAuthStore); existing stores don't need changes in Phase 2
+- `frontend/src/components/layout/AppSidebar.vue` — conditionally show `/admin` link based on `useAuthStore().user?.role`
+
+### Key Constraints from Phase 1
+- All CORS currently `["*"]` — must update `CORSMiddleware` in `main.py` (Phase 2 D-09)
+- `SECRET_KEY` is already in `.env.example` and `config.py` with default `"CHANGEME"` — Phase 2 reads it for JWT signing
+- Rate limiting store: Redis is already wired as the Celery broker — also use it for rate limit counters (no second Redis needed per D-09)
+- `documents.user_id` stays nullable — do NOT add NOT NULL in Phase 2 (Phase 3 migration adds it)
+
+
+
+
+## Specific Ideas
+
+- PyOTP `valid_window=1` recommended (from STATE.md Open Questions) — allows ±30s clock drift without expanding the replay window excessively
+- Audit existing codebase for any `passlib`/`bcrypt` usage before removing them (STATE.md note)
+- Reset token format: signed JWT (separate short-lived token, not a session token) with `sub=user_id`, `type=password-reset`, `exp=now+3600`
+- Backup codes: 8–10 codes, each 8–10 alphanumeric chars, stored as Argon2 hashes in a `backup_codes` table (or JSONB in `users` — researcher should check best practice)
+- TOTP replay prevention: store used `(user_id, code, validity_window_bucket)` tuples in Redis with TTL = TOTP validity window (60s for `valid_window=1`)
+
+
+
+
+## Deferred Ideas
+
+None — discussion stayed within phase scope.
+
+
+
+---
+
+*Phase: 2-Users & Authentication*
+*Context gathered: 2026-05-22*
diff --git a/.planning/phases/02-users-authentication/02-DISCUSSION-LOG.md b/.planning/phases/02-users-authentication/02-DISCUSSION-LOG.md
new file mode 100644
index 0000000..1b67e77
--- /dev/null
+++ b/.planning/phases/02-users-authentication/02-DISCUSSION-LOG.md
@@ -0,0 +1,153 @@
+# Phase 2: Users & Authentication - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-05-22
+**Phase:** 2-Users & Authentication
+**Areas discussed:** Email transport, Admin bootstrap, API auth scope, Frontend auth UX
+
+---
+
+## Email Transport
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| SMTP via env vars | SMTP_HOST/PORT/USER/PASSWORD/FROM env vars; dev fallback to stdout | ✓ |
+| Resend / Mailgun API | Third-party transactional email API; one env var (API key) | |
+| Console-only for now | Print reset link to stdout only; not production-ready | |
+
+**User's choice:** SMTP via env vars
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Log reset link to stdout | Developer sees link in docker compose logs; zero extra config | ✓ |
+| Return token in API response | Token in JSON response when SMTP not set; easier for testing | |
+| Return 503 | Hard failure if SMTP not configured; breaks local dev | |
+
+**User's choice:** Log the reset link to backend stdout when SMTP not configured
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Celery task (async) | Email enqueued; API returns 202 immediately; Redis already wired | ✓ |
+| Synchronous inline | Email sent during request; SMTP timeout blocks user | |
+
+**User's choice:** Celery task — async dispatch, 202 returned immediately
+
+---
+
+## Admin Bootstrap
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| ENV-var bootstrap on startup | ADMIN_EMAIL + ADMIN_PASSWORD; seeded on startup if no users exist; idempotent | ✓ |
+| Alembic seed migration | Admin row inserted in migration; credentials from env at migration time | |
+| CLI management command | Manual python manage.py create-admin step after deploy | |
+| First-user-is-admin | First registered user becomes admin; race-condition risk | |
+
+**User's choice:** ENV-var bootstrap on startup
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Skip silently if not set | App starts normally; admin can be created later | |
+| Warn in startup logs if not set | WARNING logged; app starts; helpful reminder | ✓ |
+| Refuse to start if not set | Startup fails; blocks headless/CI environments | |
+
+**User's choice:** Warn in startup logs (app still starts)
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Same 100 MB default quota | Consistent; every users row gets a quotas row | ✓ |
+| Unlimited quota for admin | Signals admins are operators, not subject to storage limits | |
+| No quota row for admin | Admin doesn't upload; but requires special-casing in enforcement | |
+
+**User's choice:** Same 100 MB default quota as regular users
+
+---
+
+## API Auth Scope
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Only new auth endpoints; existing stay public | Phase 2 ships /api/auth/* only; D-03 nullable user_id preserved | ✓ |
+| Lock down all endpoints in Phase 2 | All endpoints get get_current_user + NOT NULL migration | |
+
+**User's choice:** Only new auth endpoints; existing document/topics/settings stay public
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| /api/admin/* prefix | All admin under /api/admin/; get_current_admin on every handler | ✓ |
+| /api/users/* with role check | Shared prefix, role-based access control | |
+| /admin/* separate mount | Separate path, possible separate FastAPI sub-application | |
+
+**User's choice:** /api/admin/* prefix with get_current_admin dependency
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| CORS_ORIGINS env var (comma-separated) | Pydantic parses as list; default localhost:5173 | ✓ |
+| Hardcode origins per environment | Config file per environment or build-time injection | |
+| Keep allow_origins=['*'] | Not recommended with auth cookies now in play | |
+
+**User's choice:** CORS_ORIGINS env var, comma-separated
+
+---
+
+## Frontend Auth UX
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Full auth wall | beforeEach guard redirects to /login if no access token | ✓ |
+| Partial — auth only gates account/admin | Documents/topics still accessible without login | |
+
+**User's choice:** Full auth wall — entire app requires login
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| useAuthStore + fetch interceptor | In-memory token; auto-refresh on 401; redirect on refresh failure | ✓ |
+| Token in composable with manual headers | More explicit but repetitive across API calls | |
+| You decide | Defer to downstream agents | |
+
+**User's choice:** useAuthStore with in-memory token + fetch interceptor
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| /admin route, separate AdminView | Route guard (role=admin); sub-navigation; sidebar link conditionally shown | ✓ |
+| Integrated into /settings as Admin tab | Mixes admin and personal settings | |
+| You decide | Defer to UI researcher | |
+
+**User's choice:** /admin route with dedicated AdminView; sidebar link visible for admin role only
+
+---
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Full TOTP enrollment UI in Phase 2 | QR code + backup codes + acknowledgment; aligned with success criteria | ✓ |
+| Backend-only; UI deferred to Phase 3 | Misaligns with Phase 2 success criterion #2 | |
+
+**User's choice:** Full TOTP enrollment UI in Phase 2
+
+---
+
+## Claude's Discretion
+
+None — all areas had a clear user preference.
+
+## Deferred Ideas
+
+None — discussion stayed within phase scope.