Files
kite/.planning/research/STACK.md
T

25 KiB
Raw Blame History

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


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:

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:

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

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:

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:

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

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


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