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