--- gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone current_phase: 3 status: executing last_updated: "2026-05-23T14:49:20.062Z" progress: total_phases: 5 completed_phases: 2 total_plans: 15 completed_plans: 13 percent: 40 --- # Project State **Project:** DocuVault **Status:** Phase 3 In Progress — Plan 03 Complete **Current Phase:** 3 **Last Updated:** 2026-05-23 ## Phase Status | Phase | Name | Status | |---|---|---| | 1 | Infrastructure Foundation | ✓ Complete | | 2 | Users & Authentication | ✓ Complete (5/5 plans) | | 3 | Document Migration & Multi-User Isolation | In Progress (3/5 plans complete) | | 4 | Folders, Sharing, Quotas & Document UX | Not Started | | 5 | Cloud Storage Backends | Not Started | ## Current Position **Phase:** 03-document-migration-multi-user-isolation — In Progress **Plan:** 3/5 complete (Plan 03: Per-user document/topic isolation, get_regular_user, admin topics endpoint) **Progress:** ████░░░░░░ 52% (2/5 phases complete, 13/15 plans done) ## Performance Metrics | Metric | Value | |---|---| | Phases complete | 1 / 5 | | Requirements mapped | 54 / 54 | | Plans written | 5 (Phase 1) | | Plans complete | 10 (5 Phase 1 + 5 Phase 2) | ## Accumulated Context ### Key Decisions | Decision | Rationale | |---|---| | PostgreSQL + MinIO | Multi-user quotas and horizontal scaling require shared, consistent state | | HKDF per-user key derivation | Single Fernet key would be catastrophic on leak — must be derived before first credential is stored | | Presigned MinIO URL flow | FastAPI handles metadata only; bytes never pass through the API layer | | Atomic PostgreSQL quota UPDATE | Never perform quota arithmetic in Python between two DB statements | | JWT in httpOnly cookie | Refresh token in httpOnly cookie; access token in Pinia memory only — never localStorage | | Refresh token family revocation | RFC 9700 — reuse of a rotated token revokes entire family and alerts user | | BackgroundTasks replacement | FastAPI BackgroundTasks is per-instance; replace with Celery+Redis or pgqueuer before horizontal scale | | AuditLog metadata_ ORM attribute | `metadata` is reserved on DeclarativeBase; ORM attribute is `metadata_` with `name="metadata"` kwarg to avoid silent collision | | documents.user_id nullable Phase 1 | D-03 — no auth in Phase 1; Phase 2 migration adds NOT NULL after auth lands | | groups stub table Phase 1 | D-02 — groups is a v2 feature; table created now for schema completeness, no rows until Phase 2+ | | SEQUENCES grants in migration | GRANT USAGE/SELECT on sequences required for audit_log.id autoincrement nextval() by docuvault_app | | Admin impersonation excluded | Explicit architectural exclusion — no endpoint or UI pathway; violates privacy-first core value | | user_id as refresh token family proxy | No separate family_id column; user_id serves as family per RFC 9700 — simpler schema | | pwdlib over passlib | pwdlib actively maintained with clean Argon2Hasher API; passlib unmaintained | | TOTP replay TTL=90s | valid_window=1 covers ±30s (90s total) — TTL matches window | | HIBP fail-open | Network errors return False + log warning; auth never blocked by external service | | Two-DSN PostgreSQL strategy | DATABASE_URL (docuvault_app, DML only) + DATABASE_MIGRATE_URL (docuvault_migrate, DDL only); celery-worker gets only DATABASE_URL | | MinIO healthcheck via mc ready local | curl removed from MinIO Docker image since Oct 2023; mc is the correct in-container healthcheck tool | | pydantic-settings v2 SettingsConfigDict | SettingsConfigDict API used (not deprecated class Config form) for env var config | | async_client fixture name | Distinct from legacy sync `client` fixture to avoid collision; both coexist until Plan 05 | | xfail(strict=False) for Wave 0 | All pre-implementation scaffolds use strict=False so unexpected passes don't break CI | | StorageBackend ABC + factory mirrors ai/ pattern | 5 abstract methods; get_storage_backend() factory; MinIOBackend wraps all sync Minio SDK calls in asyncio.to_thread() | | STORE-02 key enforced in code | MinIOBackend.put_object constructs {user_id}/{document_id}/{uuid4()}{ext}; no filename parameter — only extension passes through | | null-user D-03 sentinel | services/storage.save_upload uses user_id="null-user" in Phase 1 (no auth); Phase 2 replaces with str(current_user.id) | | load_settings flat-file Phase 1 | users.ai_provider/ai_model columns cannot be populated until Phase 2; settings remain flat-file JSON for Phase 1 | | Deferred Celery import in /password-reset | send_reset_email.delay called via from tasks.email_tasks import send_reset_email inside handler body — same circular-import fix as document_tasks | | TOTP QR code as otpauth:// link | No QR library installed; plan permits manual secret display for MVP; functional flow complete without rendered QR image | | ConfirmBlock no acknowledgment checkbox | ConfirmBlock handles message + button pair; BackupCodesDisplay owns its separate acknowledgment checkbox — no overlap | | ADMIN-07 enforced by omission | No impersonation endpoint exists; AST check + test_admin_impersonation_not_found verify absence; violates privacy-first core value | | _user_to_dict() whitelist for admin responses | Explicit field whitelist prevents accidental password_hash/credentials_enc leakage from admin endpoints | | Quota warning is 200 not 4xx | Below-usage limit change is applied; warning=True advisory field returned — not a rejection | | AdminQuotasTab fetches quotas per-user via Promise.allSettled | adminListUsers() does not include quota fields; per-user endpoint parallelized; failed quotas filtered silently | | Temp password via crypto.getRandomValues | Browser-native CSPRNG; no external library; always satisfies AUTH-01 strength rules | | batch_alter_table for NOT NULL in migration 0003 | SQLite requires batch_alter_table for ALTER COLUMN; transparent passthrough on PostgreSQL — enables SQLite CI test runs | | MinIO step in migration 0003 gated on MINIO_ENDPOINT | Migration skips MinIO deletions when env var absent; enables safe SQLite test runs per T-03-02 | | raising=False for Phase 3 MinIO mock fixtures | mock_minio_presigned + mock_minio_stat patch methods that don't exist until Plan 03-02; raising=False pre-installs them | | Dual MinIO client (internal + public) | Presigned URL HMAC signature must be computed with browser-visible hostname (localhost:9000); using internal Docker client (minio:9000) causes browser signature mismatch | | Wave 2 user_id=None guard | upload-url sets user_id=None + object_key "null-user/" prefix; confirm skips quota when user_id is None; Plan 03-03 removes both guards | | SQLite quota xfail(strict=False) | SQLite stores UUID as CHAR(32) without dashes; raw SQL WHERE user_id = :uid never matches str(uuid) dashed format — test-env limitation, not code defect | | Celery mock required in /confirm tests | extract_and_classify.delay() connects to Redis; monkeypatch blocks it in unit tests; MagicMock pattern established for all confirm endpoint tests | | get_regular_user raises 403 for admin | Admin is authenticated but must not access document content; 401 would falsely imply unauthenticated — 403 is correct for role rejection | | Cross-user doc access returns 404 not 403 | Combining "not found" and "wrong owner" into 404 prevents attacker from learning which doc IDs exist for other users (D-16, T-03-11) | | CASE WHEN replaces GREATEST in quota decrement | SQLite lacks GREATEST scalar function; CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END is semantically equivalent and SQLite-compatible | | load_topics_for_user uses or_(user_id == x, user_id.is_(None)) | SQLAlchemy is_(None) not == None; or_() combines system topics and user's own topics for namespace-scoped query (D-17, DOC-04) | | AI-suggested topics go in user namespace | classifier passes user_id=doc.user_id to create_topic; AI-suggested topics are per-user not system-wide (D-11) | ### Open Questions - Verify cloud SDK minor versions on PyPI before Phase 5 pinning ### Blockers None. ## Session Continuity _Updated at each phase transition._ | Field | Value | |---|---| | Last session | 2026-05-23 — Executed Plan 03-03 (per-user document/topic isolation, get_regular_user dep, admin topics endpoint) | | Next action | Run `/gsd:execute-phase 3` to execute Plan 03-04 | | Pending decisions | None | | Resume file | `.planning/phases/03-document-migration-multi-user-isolation/03-04-PLAN.md` |