# Stack Research — DocuVault: Multi-User Auth, Storage & Cloud Integrations

**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)

---

## Existing Stack (Do Not Replace)

| 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 |

---

## Area 1: Authentication

### JWT — PyJWT 2.12.1

**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`)

```
pip install "pyjwt[crypto]>=2.12.1"
```

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).

**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.

### 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"
```

**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 (7–30 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.

---

## Area 2: Database

### ORM — SQLAlchemy 2.0 (async) + psycopg (v3)

**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)

```
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)

### MinIO Python SDK 7.x

**Confidence: MEDIUM** (MinIO SDK is well-known; exact version from training data — verify on PyPI before pinning)

```
pip install "minio>=7.2.0"
```

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.

---

## Area 4: Cloud Storage SDKs

### OneDrive — msgraph-sdk 1.x + azure-identity 1.x

**Confidence: MEDIUM** (Microsoft Graph Python SDK is GA per official Microsoft docs; exact version from training data — verify on PyPI)

```
pip install "msgraph-sdk>=1.0.0" "azure-identity>=1.19.0"
```

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

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
    },
  },
})
```

### Token Storage Strategy

**Confidence: HIGH** (security best practice, not library-specific)

- **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.

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.

### 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
```

```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 } }
    }
  }
})
```

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.

---

## Full Dependency Summary

### Python (backend)

```
# requirements.txt additions for this milestone

# 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

# Database
sqlalchemy[asyncio]>=2.0.36
psycopg[asyncio,binary]>=3.2.0
alembic>=1.14.0

# 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"
  }
}
```

---

## Alternatives Considered

| 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 |

---

## What NOT to Use

| 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 |

---

## Stack Compatibility Notes

| 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. |

---

## Version Compatibility Matrix

| 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 | — | — | — |

---

## 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

---
*Stack research for: DocuVault multi-user auth, PostgreSQL, MinIO, cloud integrations*
*Researched: 2026-05-21*
