docs: define milestone v0.2 requirements (36 reqs, 6 categories)

UI overhaul, codebase quality, admin panel rearchitecture, UX
improvements, responsive layout, and performance/stack updates.
Research: STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-07 09:22:40 +02:00
co-authored by Claude Sonnet 4.6
parent fd05563ec6
commit f7758776ec
7 changed files with 1270 additions and 2357 deletions
+239 -470
View File
@@ -1,553 +1,322 @@
# Stack Research — DocuVault: Multi-User Auth, Storage & Cloud Integrations
# Technology Stack — DocuVault v0.2: UI Overhaul and Optimization
**Domain:** SaaS document management — adding multi-user auth, PostgreSQL, MinIO, cloud storage integrations to existing FastAPI + Vue 3 app
**Researched:** 2026-05-21
**Overall Confidence:** MEDIUM-HIGH (most core library choices verified against official FastAPI docs and release notes; cloud SDK versions partially from training data, flagged where unverified)
**Milestone:** v0.2 — UI Overhaul and Optimization
**Researched:** 2026-06-07
**Scope:** New additions only. The v0.1 stack (FastAPI, Vue 3, Pinia, Vue Router 4, Vite, Tailwind CSS, PostgreSQL, MinIO) is unchanged and not re-researched here.
---
## Existing Stack (Do Not Replace)
## Existing Stack Baseline (Confirmed, Do Not Change)
| Component | Current | Notes |
|-----------|---------|-------|
| Backend framework | FastAPI 0.136.1 | Latest confirmed from official release notes |
| Frontend framework | Vue 3 | Keep as-is |
| Runtime | Python 3.11+ | FastAPI supports 3.14t as of 0.136.0 |
| Deployment | Docker Compose | Remains primary target |
| ASGI server | Uvicorn (via `fastapi run`) | Starlette 1.0.0 now bundled |
| Component | Current Version (package.json / requirements.txt) | Notes |
|-----------|--------------------------------------------------|-------|
| Vue | `^3.4.0` | Must bump to `^3.5.0` (see VueUse note below) |
| Vite | `^5.2.0` | Must bump to `^6.0.0` — Vite 8 is latest; skip 8 (too new) |
| Tailwind CSS | `^3.4.0` | Stay on v3; v4 is out but requires config rewrite |
| Pinia | `^2.1.0` | No change needed |
| Vue Router | `^4.3.0` | No change needed |
---
## Area 1: Authentication
## New Frontend Dependencies
### JWT — PyJWT 2.12.1
### 1. VueUse — `@vueuse/core` + `@vueuse/integrations`
**Confidence: HIGH** (verified from FastAPI release notes: `pyjwt` bumped to `2.12.1` in FastAPI 0.136.1; FastAPI tutorial now uses `import jwt` not `python-jose`)
**Recommended:** `@vueuse/core@^14.3.0`, `@vueuse/integrations@^14.3.0`
**Confidence:** HIGH (verified from Context7 docs and npm registry)
```
pip install "pyjwt[crypto]>=2.12.1"
**Why:** VueUse provides three composables this milestone needs, each saving 30-100 lines of custom code that would otherwise require careful browser-compatibility testing:
- `useDropZone` — drop-target zones for drag-and-drop file upload (handles `dragenter`/`dragleave`/`drop`, exposes `isOverDropZone` reactive ref, correct `dataTypes` filtering, Safari limitations documented)
- `onKeyStroke` — keyboard shortcut registration without manual `addEventListener`/`removeEventListener` lifecycle management
- `useSortable` (from `@vueuse/integrations`) — thin wrapper over SortableJS for list/folder reordering, auto-syncs to reactive array
**Bundle impact:** `@vueuse/core` unpacked size is ~870 KB but it is fully tree-shakeable — only imported composables are included in the bundle. Each composable is typically 1-3 KB gzipped. This is the correct way to import:
```js
import { useDropZone, onKeyStroke } from '@vueuse/core'
import { useSortable } from '@vueuse/integrations/useSortable'
```
Use `pyjwt[crypto]` to enable RS256/ES256 if asymmetric keys are ever needed. For this project HS256 with a strong secret is sufficient (single-issuer, stateless).
**Critical version constraint:** VueUse v14.0+ requires `vue@^3.5.0`. The current `package.json` pins `^3.4.0`. Vue 3.5 has no breaking changes for Options API components (confirmed from official Vue blog — purely additive release: reactivity improvements, -56% memory usage). Bump Vue to `^3.5.0` as part of this milestone's first phase.
**Do not use `python-jose`** — the FastAPI tutorial no longer references it, it has had unmaintained periods, and the official docs have migrated entirely to PyJWT.
**`@vueuse/integrations` peer deps:** `useSortable` requires `sortablejs@^1` as a peer dependency. Install `sortablejs` alongside.
### Password Hashing — pwdlib 0.2.x with Argon2
**Confidence: HIGH** (verified from current FastAPI security tutorial — `pwdlib[argon2]` is the documented recommendation, replacing the old `passlib[bcrypt]` guidance)
```
pip install "pwdlib[argon2]>=0.2.0"
```bash
npm install @vueuse/core@^14.3.0 @vueuse/integrations@^14.3.0 sortablejs@^1.15.7
npm install -D @types/sortablejs
```
**Why Argon2 over bcrypt:** Argon2id won the Password Hashing Competition, is memory-hard (resistant to GPU/ASIC attacks), and is the default recommendation in OWASP 2025 guidelines. `pwdlib` is a thin, modern wrapper; it does not carry `passlib`'s legacy baggage.
**Exception:** If the existing codebase already stores any bcrypt hashes, keep `passlib[bcrypt]` for the migration phase to verify and re-hash on login, then remove it.
### TOTP 2FA — pyotp 2.9.x
**Confidence: MEDIUM** (standard library for RFC 6238 TOTP in Python; no competing library of comparable adoption exists; version from training data — verify on PyPI before pinning)
```
pip install "pyotp>=2.9.0"
```
`pyotp` implements RFC 6238 TOTP and RFC 4226 HOTP. It generates provisioning URIs compatible with Google Authenticator, Authy, and any standard TOTP app. Generates QR code URIs via `pyotp.totp.TOTP.provisioning_uri()`. Pair with `qrcode[pil]` or `segno` to render a QR code PNG for the setup screen.
For TOTP enrollment flow:
1. Generate secret: `pyotp.random_base32()`
2. Store secret encrypted at rest (Fernet — see credential encryption below)
3. Return provisioning URI + QR code to user
4. Verify one TOTP code before marking 2FA active
5. On login: verify password first, then verify TOTP code with a 1-period window (`valid_window=1`)
### Session / Token Strategy
**Confidence: HIGH** (pattern; no external library needed beyond PyJWT)
Use a **dual-token pattern** for stateless horizontal scaling:
- **Access token**: Short-lived JWT (15 min), HS256, payload includes `user_id`, `email`, `roles`, `jti`
- **Refresh token**: Long-lived JWT (730 days), stored as `httpOnly` + `Secure` cookie, rotated on use
- **Revocation**: Store `jti` of revoked refresh tokens in PostgreSQL `token_blacklist` table with TTL. Clean up expired entries via a periodic task.
No additional session library is needed. Do not use Redis for token storage — the PROJECT.md requires stateless backends; a PostgreSQL blacklist table is sufficient for this scale and avoids another infrastructure dependency.
FastAPI's `fastapi.security.OAuth2PasswordBearer` handles the Bearer extraction from headers. Implement `get_current_user` as a dependency.
### Credential Encryption (Cloud OAuth Tokens, TOTP Secrets) — cryptography 44.x Fernet
**Confidence: HIGH** (cryptography is a stable, core Python library; Fernet is its symmetric authenticated encryption primitive)
```
pip install "cryptography>=44.0.0"
```
`cryptography.fernet.Fernet` provides AES-128-CBC + HMAC-SHA256 in a single call. Key lives in an env var (`FERNET_KEY`), never in the database. Encrypt per-user cloud OAuth tokens and TOTP secrets before writing to PostgreSQL. This satisfies the PROJECT.md privacy constraint: admin queries never see plaintext credentials.
**Key derivation pattern:** Generate one `Fernet.generate_key()` at deploy time, store in `CREDENTIAL_ENCRYPTION_KEY` env var, inject via Docker Compose secrets. Do not store the key in the database or expose it through any admin endpoint.
**Do NOT use VueUse as a global plugin** (`app.use(VueUse)`) — it doesn't have one. Import composables individually, always.
---
## Area 2: Database
### 2. vue-virtual-scroller — `vue-virtual-scroller`
### ORM — SQLAlchemy 2.0 (async) + psycopg (v3)
**Recommended:** `vue-virtual-scroller@^3.0.4`
**Confidence:** HIGH (verified from Context7 docs and npm registry; published 2026-05-20, actively maintained)
**Confidence: HIGH for SQLAlchemy 2.0 async; MEDIUM for psycopg v3 vs asyncpg** (SQLAlchemy 2.0 async confirmed stable; driver choice between asyncpg and psycopg 3 is functionally equivalent — see note below)
**Why:** Document lists can reach hundreds or thousands of entries per user. Without virtual scrolling, rendering all `DocumentCard` components simultaneously degrades scroll performance significantly on mobile. `vue-virtual-scroller` is the canonical Vue 3 virtual list library by Guillaume Chau (Akryum, Vue core team contributor). It ships ESM-only, is Vite-native, and requires Vue 3.3+ (compatible with our 3.5 bump).
**Components used:**
- `<RecycleScroller>` — fixed item height, highest performance. Use for the main document grid where item height is deterministic.
- `<DynamicScroller>` — variable item height. Use only if needed (e.g. list view with text previews of different lengths).
**Bundle size:** Unpacked ~461 KB; ESM-only means Rollup tree-shakes unused components. Requires importing its CSS: `import 'vue-virtual-scroller/index.css'`.
**Integration note:** Because `RecycleScroller` pools and reuses DOM nodes, every item component must be fully reactive from props alone — no internal `mounted()` side effects that assume a fresh component per item. Options API components that follow the props-down / events-up pattern (as required by the existing architecture) are fully compatible.
```bash
npm install vue-virtual-scroller@^3.0.4
```
pip install "sqlalchemy[asyncio]>=2.0.36" "psycopg[asyncio,binary]>=3.2.0"
```
**Why SQLAlchemy 2.0 over SQLModel for this project:**
SQLModel 0.0.38 (current version per FastAPI release notes) is the official recommendation for greenfield apps, but for this brownfield migration it introduces risk:
1. SQLModel does not yet have first-class async session documentation. Its `AsyncSession` support works but is inherited from SQLAlchemy and not well-documented in SQLModel's own tutorials.
2. The existing codebase already has Pydantic models for all API schemas. Adding SQLModel means maintaining a second model hierarchy (table models vs response models) which increases complexity mid-migration.
3. SQLAlchemy 2.0 `AsyncSession` with `asyncpg` or `psycopg[asyncio]` is battle-tested and the pattern used by the FastAPI full-stack template.
4. Alembic (see below) integrates directly with SQLAlchemy — the migration toolchain is native.
**Recommended pattern:**
```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
engine = create_async_engine(
"postgresql+psycopg://user:pass@db:5432/docuvault",
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
```
**asyncpg vs psycopg 3:** Both work with SQLAlchemy 2.0 async. Prefer `psycopg[asyncio,binary]` for this project because:
- psycopg 3 is the PostgreSQL-sanctioned successor to psycopg2, meaning the same package covers both sync (Alembic) and async (FastAPI) paths
- asyncpg is async-only and requires a separate sync driver for Alembic migrations
- psycopg 3 binary wheel has comparable performance to asyncpg in benchmarks
**Conflict note:** psycopg2 is incompatible with psycopg 3 (different import names: `psycopg2` vs `psycopg`). If any existing dependency pins `psycopg2`, update it. Do not install both.
### Migrations — Alembic 1.14.x
**Confidence: HIGH** (Alembic is the only migration tool for SQLAlchemy; no viable alternative)
```
pip install "alembic>=1.14.0"
```
**Async migration pattern** — Alembic's `env.py` needs special handling for async engines. Use the `run_sync` pattern:
```python
# alembic/env.py
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
def run_migrations_online():
connectable = create_async_engine(settings.DATABASE_URL)
async def run():
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
asyncio.run(run())
```
**Migration strategy for brownfield migration:**
1. Create initial migration that builds schema from scratch (new install path)
2. Create a separate data migration script that reads flat-file JSON and inserts rows
3. Run both in sequence during the deploy that replaces the existing data
---
## Area 3: Object Storage (MinIO)
### 3. Drag-and-Drop for File Upload — Native HTML5 + VueUse `useDropZone`
### MinIO Python SDK 7.x
**Recommended:** No additional library. Use `useDropZone` from `@vueuse/core` (already added above).
**Confidence:** HIGH
**Confidence: MEDIUM** (MinIO SDK is well-known; exact version from training data — verify on PyPI before pinning)
**Why not a dedicated library:** The drag-and-drop requirement for this milestone is drop-zone file upload (files dragged from OS to a browser area). `useDropZone` covers this completely. Dedicated upload libraries like `dropzone-vue` or `vue-file-uploader` are either poorly maintained, opinionated about UI, or duplicate what the existing upload flow already does. The existing backend upload endpoint and progress tracking are already implemented — adding a library that wraps both drop detection and upload would require replacing working code.
```
pip install "minio>=7.2.0"
```
**For folder/list reordering** (drag document into a different folder), use `useSortable` from `@vueuse/integrations` (already added above via SortableJS). SortableJS handles this precisely: it is a low-level, pure-DOM drag-sort library with no Vue-specific assumptions.
The MinIO Python SDK (`minio`) wraps the S3 API. It is synchronous. Use it inside FastAPI via `asyncio.to_thread()` for large streaming operations, or call it directly for short metadata operations.
**Important:** Do NOT use the MinIO SDK for high-throughput streaming (uploads/downloads of large documents). Instead, use **pre-signed URLs**:
```python
from minio import Minio
from datetime import timedelta
client = Minio(
"minio:9000",
access_key=settings.MINIO_ACCESS_KEY,
secret_key=settings.MINIO_SECRET_KEY,
secure=False, # True in production with TLS
)
# Generate upload URL (client uploads directly to MinIO, bypassing FastAPI)
url = client.presigned_put_object(
bucket_name="user-documents",
object_name=f"{user_id}/{document_id}",
expires=timedelta(minutes=15),
)
# Generate download URL
url = client.presigned_get_object(
bucket_name="user-documents",
object_name=f"{user_id}/{document_id}",
expires=timedelta(minutes=60),
)
```
Pre-signed URLs mean FastAPI never proxies document bytes — only metadata flows through the backend. This is critical for horizontal scaling (no file pinning to a specific backend instance) and for quota enforcement (track bytes at upload-record creation time, not at streaming time).
**Quota enforcement pattern:**
1. Client requests an upload token from FastAPI
2. FastAPI checks current usage against `user.quota_used_bytes` + `user.quota_limit_bytes`
3. If within quota, record tentative size, issue pre-signed PUT URL
4. After successful upload, confirm actual size (via MinIO event or HEAD request) and commit to quota
**boto3 alternative:** `boto3` works against MinIO via `endpoint_url` override. Only use it if you anticipate migrating to AWS S3 — for a MinIO-only deployment the native SDK is simpler and avoids the large boto3 dependency tree.
### aiobotocore / aiominio — Do Not Use
The async MinIO/S3 client libraries (`aiobotocore`, `aiominio`) add significant complexity with uncertain maintenance status. The pre-signed URL pattern renders them unnecessary — the sync SDK is only called in the FastAPI path for URL generation (microseconds), not for streaming.
**Do NOT use `vue-draggable-plus` (0.6.1, last published 2026-01-11):** It wraps SortableJS but adds a Vue component abstraction layer that conflicts with the project's data-provider view pattern. It also adds bundle overhead that `useSortable` avoids. The last publish date of January 2026 is acceptable for a stable library, but there is no reason to prefer it over the VueUse integration that is already in the bundle.
---
## Area 4: Cloud Storage SDKs
### 4. Keyboard Shortcuts — VueUse `onKeyStroke`
### OneDrive — msgraph-sdk 1.x + azure-identity 1.x
**Recommended:** `onKeyStroke` from `@vueuse/core` (already added). No additional library.
**Confidence:** HIGH
**Confidence: MEDIUM** (Microsoft Graph Python SDK is GA per official Microsoft docs; exact version from training data — verify on PyPI)
**Why no dedicated library:** Libraries like `vue-shortkey` or `hotkeys-js` exist but are unnecessary overhead. `onKeyStroke` from VueUse handles:
- Single key listening: `onKeyStroke('/', handler)` (focus search)
- Modifier combos: check `event.ctrlKey` / `event.metaKey` inside the handler
- Automatic cleanup when the component is unmounted (it wraps `addEventListener`/`removeEventListener` with `onUnmounted`)
```
pip install "msgraph-sdk>=1.0.0" "azure-identity>=1.19.0"
```
For a document manager, typical shortcuts are: `/` (search focus), `Escape` (close modal/panel), `Delete` (delete selected), `Enter` (open/confirm). All fit `onKeyStroke` with no wrapper library.
Microsoft Graph Python SDK (`msgraph-sdk`) is the official Microsoft library for OneDrive access. It covers:
- Drive item CRUD (`/me/drive/items/{id}`)
- Upload sessions for large files
- Delta sync for listing changes
**Implementation pattern:**
```js
// In setup() or a composable
import { onKeyStroke } from '@vueuse/core'
For server-side (backend-behalf-of-user) flows use the **OAuth 2.0 Authorization Code** flow with `azure-identity`'s `OnBehalfOfCredential` or a custom token provider wrapping stored refresh tokens.
**Important:** Microsoft's OneDrive tokens (access + refresh) must be stored encrypted at rest using the Fernet approach described in Area 1. Refresh tokens are long-lived and grant significant access.
**Package note:** The older `O365` package and `office365-REST-python-client` both wrap Graph API but are community-maintained. Prefer the official `msgraph-sdk` which Microsoft now actively develops and tests against Graph v1.0.
### Google Drive — google-api-python-client 2.x + google-auth-oauthlib 1.x
**Confidence: MEDIUM** (package names confirmed from Google Cloud docs; exact minor versions from training data)
```
pip install "google-api-python-client>=2.150.0" "google-auth-oauthlib>=1.2.0" "google-auth-httplib2>=0.2.0"
```
Use the Drive API v3 (not v2 — v2 is deprecated). For server-side OAuth flows:
- Use `google_auth_oauthlib.flow.Flow` for the authorization redirect
- Store OAuth2 credentials (`Credentials` object JSON) encrypted in PostgreSQL
- Rebuild credentials from stored JSON on each API call: `google.oauth2.credentials.Credentials.from_authorized_user_info(json_data, scopes)`
Required scopes for this project: `https://www.googleapis.com/auth/drive.file` (access only files created by the app — minimum privilege).
### Nextcloud — webdav4 0.x
**Confidence: MEDIUM** (webdav4 is the most actively maintained Python WebDAV client as of 2024; version from training data)
```
pip install "webdav4[fsspec]>=0.9.8"
```
Nextcloud exposes two APIs: WebDAV (for file operations) and OCS (for sharing, users, and metadata). For document upload/download, WebDAV is sufficient. `webdav4` wraps the WebDAV protocol with a clean interface and optional `fsspec` integration.
**Nextcloud-specific paths:**
- WebDAV root: `https://{host}/remote.php/dav/files/{username}/`
- Authentication: Basic auth (username + app password) or Bearer token
For Nextcloud, recommend storing an **app password** (user-generated in Nextcloud settings) rather than OAuth tokens — it's simpler to implement and doesn't require an OAuth app registration.
**webdavclient3 alternative:** An older library with less active maintenance. `webdav4` is preferred.
### Generic WebDAV — webdav4 (same package)
`webdav4` handles generic RFC 4918 WebDAV, so any WebDAV-compatible server (ownCloud, Seafile WebDAV bridge, etc.) is covered by the same adapter.
---
## Area 5: Storage Abstraction
### Pattern — Protocol-based Adapter (no third-party library needed)
**Confidence: HIGH** (this is the architecture mandated by PROJECT.md and mirrors the existing AI provider pattern)
Define a `StorageBackend` Protocol that all adapters implement:
```python
from typing import Protocol, AsyncIterator
class StorageBackend(Protocol):
async def put_object(
self,
path: str,
data: AsyncIterator[bytes],
size: int,
content_type: str,
) -> None: ...
async def get_object(self, path: str) -> AsyncIterator[bytes]: ...
async def delete_object(self, path: str) -> None: ...
async def list_objects(self, prefix: str) -> list[str]: ...
async def get_presigned_url(self, path: str, expires_seconds: int) -> str | None: ...
```
Concrete implementations:
- `MinIOBackend` — uses the MinIO SDK + pre-signed URLs
- `OneDriveBackend` — uses `msgraph-sdk`
- `GoogleDriveBackend` — uses `google-api-python-client`
- `NextcloudBackend` — uses `webdav4`
The `get_presigned_url` method returns `None` for backends that don't support it (Google Drive, Nextcloud). FastAPI then falls back to proxying the stream through the backend for those cases.
**No FSSpec dependency at the protocol layer** — FSSpec (`fsspec`) can be used internally by `webdav4` but should not leak into the storage abstraction interface. The interface must be async-native.
**Per-user backend resolution:** Store `user.storage_backend_type` (enum: `minio`, `onedrive`, `gdrive`, `nextcloud`) and `user.storage_backend_credential_id` (FK to encrypted credentials table) in PostgreSQL. A `StorageBackendFactory` resolves the correct adapter on each request.
---
## Area 6: Vue 3 Auth Patterns
### State Management — Pinia 2.x
**Confidence: HIGH** (Pinia is the official Vue 3 state management library per vuejs.org; Vuex is deprecated for Vue 3)
```
npm install pinia@^2.0.0
```
Store auth state in a Pinia store:
```typescript
// stores/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
accessToken: null as string | null,
user: null as User | null,
}),
getters: {
isAuthenticated: (state) => !!state.accessToken,
},
actions: {
setTokens(accessToken: string) {
this.accessToken = accessToken
// Refresh token is httpOnly cookie — not stored in JS
},
logout() {
this.accessToken = null
this.user = null
},
},
onKeyStroke('/', (e) => {
e.preventDefault()
searchInput.value?.focus()
})
```
### Token Storage Strategy
---
**Confidence: HIGH** (security best practice, not library-specific)
### 5. Loading Skeletons — Pure Tailwind CSS
- **Access token:** Store in Pinia memory state only (not `localStorage`, not `sessionStorage`). Survives tab navigation but is cleared on page refresh — intentional for security.
- **Refresh token:** Store as `httpOnly; Secure; SameSite=Strict` cookie set by FastAPI. Never readable by JavaScript. Refresh is done by hitting a `/auth/refresh` endpoint which reads the cookie server-side.
- **Do not use `localStorage` for tokens** — XSS vulnerability. In a document management app users upload arbitrary files; stored XSS risk is not theoretical.
**Recommended:** No library. Use `animate-pulse` utility with placeholder shapes.
**Confidence:** HIGH
On page load/refresh, immediately call `/auth/me` (which uses the httpOnly refresh cookie automatically). If it returns 200, restore access token from the response. If 401, redirect to login.
**Why no library:** Skeleton loaders in this app are simple rectangular placeholders — cards, list rows, and a few text lines. Tailwind's `animate-pulse` combined with `bg-gray-200 dark:bg-gray-700` rounded shapes is sufficient and produces zero additional bundle weight. A skeleton library (`vue3-skeleton`, etc.) would add a dependency for something that is 5 lines of Tailwind HTML per component.
### Protected Routes — Vue Router 4.x Navigation Guards
**Confidence: HIGH** (Vue Router 4 is the Vue 3 router; this is a standard pattern)
```
npm install vue-router@^4.0.0
**Recommended pattern for a document card skeleton:**
```html
<div class="animate-pulse rounded-lg border border-gray-200 p-4">
<div class="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div class="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
```
```typescript
// router/index.ts
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
// Attempt silent refresh before redirecting
try {
await auth.silentRefresh() // hits /auth/refresh endpoint
} catch {
return { name: 'login', query: { redirect: to.fullPath } }
}
}
For a shimmer effect (more polished than pulse), use a CSS `@keyframes` background-position animation — still zero JS dependency. Only add a library if shimmer is required across 10+ distinct skeleton shapes that need consistent management.
---
### 6. Tailwind CSS Plugins
**Recommended:** `@tailwindcss/forms@^0.5.11`
**Confidence:** HIGH (official Tailwind Labs plugin, 4,500+ GitHub stars, supports both v3 and v4)
**Why `@tailwindcss/forms`:** DocuVault has several form-heavy screens (login, registration, upload dialog, admin user management, cloud credential entry). Browser defaults for `<input>`, `<select>`, `<textarea>`, `<checkbox>`, and `<radio>` are inconsistent across platforms and resist Tailwind utility styling. `@tailwindcss/forms` provides a minimal, accessible reset that makes all form elements style-consistent and fully overridable with utilities. Unpacked size is 55 KB; it is a PostCSS plugin with zero runtime JS.
```bash
npm install -D @tailwindcss/forms@^0.5.11
```
```js
// tailwind.config.js
plugins: [require('@tailwindcss/forms')]
```
**Container queries (`@tailwindcss/container-queries`):** Do NOT add. In Tailwind v3, this would be the `@tailwindcss/container-queries` plugin. However, the v0.2 responsive layout target is standard viewport-based breakpoints (`sm:`, `md:`, `lg:`). Container queries are useful when the same component is rendered in contexts of different widths (sidebar vs. main content), but the DocuVault component architecture uses the data-provider view pattern where layout is controlled by the view, not by individual components. This eliminates the primary use case for container queries in this codebase. If a future phase introduces a component genuinely embedded in variable-width containers, add the plugin then.
**Typography plugin (`@tailwindcss/typography`):** Do NOT add. There is no long-form rich text content in this app — document content is displayed in a preview iframe, not rendered as styled HTML.
---
### 7. Bundle Analysis — `rollup-plugin-visualizer`
**Recommended:** `rollup-plugin-visualizer@^7.0.1` (dev dependency, build-time only)
**Confidence:** MEDIUM-HIGH (standard tool, actively maintained, Vite-native)
**Why:** The bundle size reduction goal requires knowing what is currently in the bundle. `rollup-plugin-visualizer` generates an interactive treemap that shows each module's contribution. Run once at the start of v0.2 to establish baseline, and again after refactoring to verify improvement. It does not affect the production build.
```bash
npm install -D rollup-plugin-visualizer@^7.0.1
```
```js
// vite.config.js — enable only when analyzing, not in every build
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
process.env.ANALYZE ? visualizer({ open: true, filename: 'dist/stats.html' }) : null,
].filter(Boolean),
})
```
Mark routes with `meta: { requiresAuth: true }`. The guard attempts a silent refresh before redirecting — this handles the page-refresh case where the access token is gone but the refresh cookie is still valid.
### Refresh Token Handling — Axios Interceptors
**Confidence: HIGH** (standard pattern for token refresh in SPA + REST API; Axios is already common in Vue 3 projects)
```
npm install axios@^1.0.0
```
```typescript
// api/client.ts
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true
await authStore.silentRefresh()
error.config.headers['Authorization'] = `Bearer ${authStore.accessToken}`
return axiosInstance(error.config)
}
return Promise.reject(error)
}
)
```
### TOTP UI — No dedicated library needed
The TOTP enrollment flow only requires:
1. Display a QR code image (returned as base64 PNG from FastAPI, rendered via `<img :src="qrDataUrl">`)
2. An OTP input field (6-digit numeric input, `type="text" inputmode="numeric" maxlength="6"`)
No Vue TOTP component library is needed. Avoid heavy auth UI libraries (Auth0 components, etc.) — they assume SSO flows incompatible with this design.
Run with: `ANALYZE=1 npm run build`
---
## Full Dependency Summary
## Vite Upgrade
### Python (backend)
**Recommended:** Bump from `^5.2.0` to `^6.4.3` (the stable Vite 6 release; avoid Vite 7/8 beta)
**Confidence:** HIGH (Vite 6 released 2024-11; Vite 7 and 8 are in beta as of 2026-06)
```
# requirements.txt additions for this milestone
**Why Vite 6 over staying on Vite 5:** Vite 6 is a stable, minimal-breaking-change major release. Breaking changes for this project are:
1. **Node.js ≥ 20.0.0 required** — Docker Compose service should already use Node 20+ (verify `FROM node:20-alpine` or equivalent in any Dockerfile that runs the frontend build).
2. **No other breaking changes apply** to this project (CSS library output file rename only affects library authors, not app builds; SSR CSS change is not applicable).
# Auth
pyjwt[crypto]>=2.12.1
pwdlib[argon2]>=0.2.0
pyotp>=2.9.0
cryptography>=44.0.0
qrcode[pil]>=8.0.0 # TOTP QR code generation
Vite 6 delivers improved dependency pre-bundling and module runner improvements relevant to the performance goals. The built-in code splitting (`import()` dynamic imports for lazy routes) and tree-shaking require no config changes — they are on by default. The manual chunking config for vendor splitting (Pinia, Vue Router) in `build.rollupOptions.output.manualChunks` can be used if the baseline analysis shows large vendor chunks being repeated across async routes.
# Database
sqlalchemy[asyncio]>=2.0.36
psycopg[asyncio,binary]>=3.2.0
alembic>=1.14.0
**Do NOT upgrade to Vite 7 or 8** — both are in beta as of this research date and are not appropriate for a production milestone.
# Object storage
minio>=7.2.0
# Cloud storage
msgraph-sdk>=1.0.0
azure-identity>=1.19.0
google-api-python-client>=2.150.0
google-auth-oauthlib>=1.2.0
google-auth-httplib2>=0.2.0
webdav4>=0.9.8
```
### JavaScript (frontend)
```json
{
"dependencies": {
"pinia": "^2.0.0",
"vue-router": "^4.0.0",
"axios": "^1.0.0"
}
}
```bash
npm install -D vite@^6.4.3 @vitejs/plugin-vue@^5.0.0
```
---
## Alternatives Considered
## Vue Version Bump
| Category | Recommended | Alternative | Why Not |
|----------|-------------|-------------|---------|
| JWT | PyJWT 2.12.1 | python-jose | FastAPI docs migrated away; python-jose had unmaintained periods; PyJWT is the Python JWT spec reference implementation |
| Password hashing | pwdlib + Argon2 | passlib + bcrypt | passlib is in maintenance mode; bcrypt is weaker than Argon2 (not memory-hard); pwdlib is the current FastAPI recommendation |
| ORM | SQLAlchemy 2.0 async | SQLModel 0.0.38 | SQLModel is great for greenfield but brownfield migration risk is higher; async SQLModel docs are thin; direct SQLAlchemy gives full control |
| ORM | SQLAlchemy 2.0 async | Tortoise ORM 0.21.x | Tortoise has its own metaclass system that conflicts with Pydantic models; integration with FastAPI requires aerich for migrations (separate toolchain); less ecosystem momentum than SQLAlchemy |
| PostgreSQL driver | psycopg 3 | asyncpg | asyncpg is async-only (needs separate sync driver for Alembic); psycopg 3 covers both paths; psycopg 3 is the official PostgreSQL Python driver successor |
| OneDrive | msgraph-sdk | O365 / office365-REST | Community-maintained; Graph API coverage incomplete; Microsoft has deprecated these in favor of the official SDK |
| S3 integration | minio native SDK | boto3 | boto3 pulls in botocore (large dep tree); minio SDK is purpose-built and simpler for MinIO-only use; boto3 makes sense only if AWS S3 migration is planned |
| Frontend state | Pinia | Vuex | Vuex is the Vue 2 store; Vue 3 official recommendation is Pinia |
| Token storage | Memory (Pinia) | localStorage | localStorage is vulnerable to XSS; document management apps with file upload have non-trivial XSS surface |
**Required:** `^3.4.0``^3.5.0`
**Confidence:** HIGH (no breaking changes for Options API; confirmed from official Vue 3.5 blog post)
This is required by `@vueuse/core@^14.0`. Vue 3.5 is a drop-in upgrade for Options API components — the release is purely additive (reactivity performance improvements, `useTemplateRef()` for Composition API). No component changes are needed.
```bash
npm install vue@^3.5.0
```
---
## What NOT to Use
## Backend — No New Python Packages Needed
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `python-jose` | No longer referenced by FastAPI docs; had maintenance gaps; `python-multipart` dependency overlap caused version conflicts | `pyjwt[crypto]` |
| `passlib[bcrypt]` for new hashes | In maintenance mode; bcrypt is not memory-hard; weaker than Argon2 against modern GPU attacks | `pwdlib[argon2]` (keep passlib only for migrating existing bcrypt hashes) |
| `Tortoise ORM` | Incompatible metaclass system creates friction with Pydantic v2; aerich migration toolchain is less mature; smaller ecosystem | SQLAlchemy 2.0 async |
| `tiangolo/uvicorn-gunicorn-fastapi` Docker image | **Deprecated** by FastAPI author as of 2024. Official FastAPI docs now recommend building from `python:3.x` base directly | Plain `python:3.12-slim` base image |
| `databases` (encode/databases) | Was an early async DB wrapper; SQLAlchemy 2.0 async has superseded its use case; the project is effectively in maintenance mode | SQLAlchemy 2.0 `AsyncSession` |
| `localStorage` for auth tokens | XSS-accessible; a document management app is an attractive XSS target | httpOnly cookies for refresh tokens; Pinia memory for access tokens |
| Multiple per-user Fernet keys | Overly complex key management; one platform-level Fernet key is sufficient — user data isolation is enforced at the PostgreSQL row level, not at the encryption key level | Single `CREDENTIAL_ENCRYPTION_KEY` env var |
**Confidence:** HIGH
FastAPI's `APIRouter` (already used) is the correct and sufficient tool for router decomposition. Splitting large router files into smaller modules uses only `APIRouter(prefix=..., tags=[...])` and `app.include_router()` — both already in the project. No new library is warranted.
**Pattern for large router decomposition:**
```
backend/routers/
documents/
__init__.py # from .upload import router as upload_router; etc.
upload.py # APIRouter for POST /documents
search.py # APIRouter for GET /documents
share.py # APIRouter for share endpoints
admin/
__init__.py
users.py
audit.py
settings.py
```
Include in `main.py`:
```python
from routers.documents import router as documents_router
from routers.admin import router as admin_router
app.include_router(documents_router, prefix="/documents", tags=["documents"])
app.include_router(admin_router, prefix="/admin", tags=["admin"])
```
This is a pure refactor — no new dependencies, no behavior change, no test changes except updating import paths.
---
## Stack Compatibility Notes
## Full v0.2 Dependency Diff
| Concern | Detail |
|---------|--------|
| Pydantic v2 required | FastAPI 0.136.x requires `pydantic>=2.9.0`. SQLAlchemy 2.0 is Pydantic v2-compatible. The existing app must already be on Pydantic v2 to run FastAPI 0.136. |
| psycopg 3 vs psycopg 2 | If the existing codebase (or any dependency) imports `psycopg2`, there will be a name conflict. `psycopg` (v3) imports as `import psycopg`, so they can technically coexist in the same environment, but avoid having both. |
| Starlette 1.0.0 | Bumped in FastAPI 0.136.1 — this is a major version. If the existing app uses any Starlette internals directly (middleware, routing), audit for breaking changes before upgrading FastAPI. |
| PyJWT 2.x vs 1.x API | PyJWT 2.x changed `jwt.encode()` to return `str` (not `bytes`). If the existing codebase has any JWT code using the 1.x API, update the call sites. |
| Vue Router 4 + Pinia SSR | Not applicable (no SSR in this project), but worth noting: Pinia's state is per-request in SSR contexts. For this SPA deployment, no issues. |
| Argon2 system dependency | `pwdlib[argon2]` requires `argon2-cffi` which needs a C compiler or binary wheel. The official Python Docker image (`python:3.12-slim`) provides wheels for common platforms — no `build-essential` needed. |
### `npm install` (production dependencies)
```bash
npm install \
vue@^3.5.0 \
@vueuse/core@^14.3.0 \
@vueuse/integrations@^14.3.0 \
sortablejs@^1.15.7 \
vue-virtual-scroller@^3.0.4
```
### `npm install -D` (dev dependencies)
```bash
npm install -D \
vite@^6.4.3 \
@vitejs/plugin-vue@^5.0.0 \
@tailwindcss/forms@^0.5.11 \
rollup-plugin-visualizer@^7.0.1 \
@types/sortablejs@^1.15.0
```
### Python backend additions
**None.** All backend work (router decomposition, service extraction) uses FastAPI primitives already present.
---
## Version Compatibility Matrix
## Alternatives Considered and Rejected
| Package | Version | Python | Pydantic | FastAPI |
|---------|---------|--------|---------|--------|
| pyjwt | 2.12.1 | 3.8+ | any | 0.100+ |
| pwdlib | 0.2.x | 3.9+ | v2 | 0.100+ |
| sqlalchemy | 2.0.36+ | 3.8+ | v2 (via fastapi) | 0.100+ |
| psycopg (v3) | 3.2.x | 3.8+ | — | — |
| alembic | 1.14.x | 3.8+ | — | — |
| minio | 7.2.x | 3.7+ | — | — |
| msgraph-sdk | 1.x | 3.8+ | — | — |
| azure-identity | 1.19.x | 3.8+ | — | — |
| pinia | 2.x | — | — | — |
| vue-router | 4.x | — | — | — |
| Category | Rejected | Reason |
|----------|----------|--------|
| Drag-and-drop file upload | `dropzone-vue`, `vue-file-uploader` | Opinionated UI, poor maintenance, duplicates existing backend upload flow |
| Drag-and-drop sorting | `vue-draggable-plus@0.6.1` | Adds Vue component abstraction over SortableJS that conflicts with data-provider view pattern; VueUse `useSortable` covers the same need with less surface area |
| Virtual scrolling | `@tanstack/vue-virtual` | Lower-level API requiring more integration code than `vue-virtual-scroller`; no meaningful bundle advantage; vue-virtual-scroller is Vue-native |
| Keyboard shortcuts | `hotkeys-js`, `vue-shortkey` | Unnecessary wrapper over `addEventListener`; VueUse `onKeyStroke` is sufficient and already in the bundle |
| Loading skeletons | `vue3-skeleton` | 5-line Tailwind pattern produces identical results; no justification for an additional dependency |
| Tailwind container queries | `@tailwindcss/container-queries` | Viewport breakpoints are sufficient for this app's layout; container queries solve a problem this codebase does not have |
| Tailwind typography | `@tailwindcss/typography` | No prose HTML content in this app |
| Vite 7 / 8 | Latest beta | Both in beta as of research date; not appropriate for a production milestone |
---
## Compatibility Matrix (v0.2 additions)
| Package | Version | Vue Requirement | Vite Compatibility | Tree-Shakeable |
|---------|---------|-----------------|-------------------|----------------|
| `@vueuse/core` | ^14.3.0 | ^3.5.0 | Vite 5+ | Yes — import individually |
| `@vueuse/integrations` | ^14.3.0 | ^3.5.0 | Vite 5+ | Yes |
| `sortablejs` | ^1.15.7 | — (plain JS) | Any | Yes |
| `vue-virtual-scroller` | ^3.0.4 | ^3.3.0 | Vite (ESM) | Partial (register components individually) |
| `@tailwindcss/forms` | ^0.5.11 | — (PostCSS) | Any | N/A (CSS plugin) |
| `rollup-plugin-visualizer` | ^7.0.1 | — (build tool) | Vite 5+ | N/A (dev only) |
| `vue` | ^3.5.0 | — | — | — |
| `vite` | ^6.4.3 | — | — | — |
---
## Sources
- FastAPI official release notes (verified 2026-05-21): https://fastapi.tiangolo.com/release-notes/ — PyJWT 2.12.1, SQLModel 0.0.38, Starlette 1.0.0, pydantic>=2.9.0 confirmed
- FastAPI security tutorial (verified 2026-05-21): https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/ — PyJWT recommended, python-jose absent, pwdlib[argon2] recommended
- FastAPI SQL databases tutorial (verified 2026-05-21): https://fastapi.tiangolo.com/tutorial/sql-databases/ — SQLModel documented as recommended ORM
- FastAPI Docker guide (verified 2026-05-21): https://fastapi.tiangolo.com/deployment/docker/ — tiangolo/uvicorn-gunicorn-fastapi deprecated confirmed
- Microsoft Graph SDK overview (verified 2026-05-21): https://learn.microsoft.com/en-us/graph/sdks/sdks-overview — Python SDK confirmed GA
- pwdlib argon2 version: MEDIUM confidence — training data, verify on PyPI
- pyotp version: MEDIUM confidence — training data, verify on PyPI
- minio Python SDK version: MEDIUM confidence — training data, verify on PyPI
- webdav4 version: MEDIUM confidence — training data, verify on PyPI
- google-api-python-client version: MEDIUM confidence — training data, verify on PyPI
- azure-identity / msgraph-sdk minor versions: MEDIUM confidence — training data, verify on PyPI
- VueUse v14.3.0 docs and peer dependencies: Context7 `/vueuse/vueuse`, verified via npm registry (2026-06-07)
- vue-virtual-scroller v3.0.4: Context7 `/akryum/vue-virtual-scroller`, npm registry (last published 2026-05-20)
- vue-draggable-plus v0.6.1: npm registry (last published 2026-01-11) — rejected
- sortablejs v1.15.7: npm registry (2026-06-07)
- Tailwind CSS container queries: official Tailwind CSS announcement — built into v4 core; plugin for v3 would be `@tailwindcss/container-queries` — rejected as out of scope
- @tailwindcss/forms: official Tailwind Labs GitHub (tailwindlabs/tailwindcss-forms), 4,500+ stars, supports v3 and v4
- rollup-plugin-visualizer: npm registry, v7.0.1 (2026-06-07)
- Vue 3.5 no-breaking-changes confirmation: https://blog.vuejs.org/posts/vue-3-5
- Vite 6 migration guide: https://v6.vite.dev/guide/migration
- FastAPI router decomposition: https://fastapi.tiangolo.com/tutorial/bigger-applications/ (built-in, no new deps)
---
*Stack research for: DocuVault multi-user auth, PostgreSQL, MinIO, cloud integrations*
*Researched: 2026-05-21*
*Stack research for: DocuVault v0.2 — UI Overhaul and Optimization*
*Researched: 2026-06-07*