554 lines
25 KiB
Markdown
554 lines
25 KiB
Markdown
# 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*
|