# Quantumancy Plan 1/7: Foundation & Auth — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Stand up the deployable skeleton of Quantumancy — repo scaffold, a local Postgres instance, and username/password auth with server-side session cookies — so every later plan (LLM pipeline, spirit modes, Codex) has a real, tested backend to build on. **Architecture:** Python + FastAPI backend (async SQLAlchemy 2.0 / asyncpg against Postgres), running from a Python venv under `uvicorn` on this Proxmox LXC CT — no containers. Postgres is installed directly via `apt` on the same CT, with two databases on the one instance: `quantumancy` (dev/prod) and `quantumancy_test` (test-only, dropped and recreated on every test run). Tables are created via `Base.metadata.create_all` at startup (no Alembic yet — schema is not stable enough to justify migration tooling at this stage; revisit once the schema stabilizes past Plan 3). A systemd unit template is included for running the app as a real service, though enabling it is a manual final step outside the automated task/test loop. **Tech Stack:** FastAPI, SQLAlchemy 2.0 (async, asyncpg), argon2-cffi, httpx, pytest + pytest-asyncio, systemd. ## Global Constraints - Backend is Python + FastAPI (spec §2). - App serves plain HTTP on port 7777; TLS is handled entirely outside this repo by a Cloudflare Tunnel (spec §2). - **No containers.** The app runs from a Python venv under `uvicorn`, managed by systemd; Postgres is installed directly via `apt` (spec §2, §9). This is a hard constraint from the user, not a stylistic preference — do not introduce Docker/Compose anywhere in this plan or its implementation. - Database is Postgres (spec §2, §5). - Auth is username/password with argon2 hashing, server-side session cookies (not JWT), and **open registration** — no invite gating (spec §5). - Per-account and per-IP rate limiting applies to all LLM-triggering endpoints (spec §5) — this plan builds the reusable limiter; wiring it onto LLM endpoints happens in Plan 2 once those endpoints exist. - Note on naming vs. spec: the spec's `sessions` table (mode used, timing) is implemented here as `contact_sessions` to avoid collision with the login-session concept (`auth_sessions`), which the spec also calls "sessions." Both concepts exist; only the domain table name changed for clarity. ## Plan Series This is 1 of 7 plans implementing the Quantumancy website spec (`docs/superpowers/specs/2026-07-20-quantumancy-website-design.md`): 1. **Foundation & Auth** (this plan) 2. LLM/TTS/Realtime pipeline (Ollama client+queue, Piper TTS+effects, WebSocket session channel, frontend scaffold) 3. Wire Ghost mode + Ouija/Planchette UI 4. EVP Listening mode 5. Spirit Radio mode 6. Codex & entity persistence 7. Internationalization (EN/ES) --- ## Task 1: Repo Scaffold, Venv, Health Check **Files:** - Create: `backend/requirements.txt` - Create: `backend/app/__init__.py` - Create: `backend/app/main.py` - Create: `backend/pytest.ini` - Create: `deploy/quantumancy.service` - Create: `.env.example` - Create: `.gitignore` - Test: `backend/tests/test_health.py` **Interfaces:** - Produces: `app` FastAPI instance in `backend/app/main.py`, importable as `app.main:app`. - [ ] **Step 1: Create the venv and infra files** Run: ```bash cd backend python3 -m venv venv source venv/bin/activate ``` `backend/requirements.txt`: ``` fastapi==0.115.0 uvicorn[standard]==0.32.0 sqlalchemy[asyncio]==2.0.36 asyncpg==0.30.0 pydantic-settings==2.6.1 argon2-cffi==23.1.0 httpx==0.27.2 numpy==2.1.3 pytest==8.3.3 pytest-asyncio==0.24.0 ``` Run: `pip install -r requirements.txt` `backend/pytest.ini`: ```ini [pytest] asyncio_mode = auto ``` `.gitignore` (repo root): ``` backend/venv/ __pycache__/ *.pyc .env frontend/node_modules/ frontend/dist/ ``` `.env.example` (repo root): ``` DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy OLLAMA_BASE_URL=http://10.30.20.107:11434 SESSION_SECRET=change-me-to-a-random-64-char-string PORT=7777 ``` `deploy/quantumancy.service`: ```ini [Unit] Description=Quantumancy web app After=network.target postgresql.service [Service] Type=simple WorkingDirectory=/root/quantumancy/backend EnvironmentFile=/root/quantumancy/.env ExecStart=/root/quantumancy/backend/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 7777 Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target ``` Note: `deploy/quantumancy.service` is not enabled in this task — that's a manual deployment step (`sudo cp deploy/quantumancy.service /etc/systemd/system/ && sudo systemctl enable --now quantumancy`) to run once the app is further along. It's created now so the repo carries its own deployment config from day one. - [ ] **Step 2: Write the failing test** `backend/tests/test_health.py`: ```python from fastapi.testclient import TestClient from app.main import app def test_healthz_returns_ok(): client = TestClient(app) response = client.get("/healthz") assert response.status_code == 200 assert response.json() == {"status": "ok"} ``` - [ ] **Step 3: Run test to verify it fails** Run: `cd backend && source venv/bin/activate && python -m pytest tests/test_health.py -v` Expected: FAIL with `ModuleNotFoundError: No module named 'app'` (or `ImportError`) since `app/main.py` doesn't exist yet. - [ ] **Step 4: Write minimal implementation** `backend/app/__init__.py`: (empty file) `backend/app/main.py`: ```python from fastapi import FastAPI app = FastAPI(title="Quantumancy") @app.get("/healthz") async def healthz(): return {"status": "ok"} ``` - [ ] **Step 5: Run test to verify it passes** Run: `cd backend && source venv/bin/activate && python -m pytest tests/test_health.py -v` Expected: PASS - [ ] **Step 6: Commit** ```bash git add backend/requirements.txt backend/app backend/pytest.ini backend/tests/test_health.py deploy/quantumancy.service .env.example .gitignore git commit -m "chore: scaffold backend venv, health check, systemd unit" ``` --- ## Task 2: Config Settings & Async DB Engine **Files:** - Create: `backend/app/config.py` - Create: `backend/app/db.py` - Test: `backend/tests/test_config.py` **Interfaces:** - Consumes: nothing from Task 1 directly (parallel infra piece). - Produces: `settings` (Settings instance) in `app.config`; `Base` (DeclarativeBase), `engine`, `async_session_maker`, `get_db()` async generator in `app.db` — every later model/route task depends on these exact names. - [ ] **Step 1: Write the failing test** `backend/tests/test_config.py`: ```python from app.config import Settings def test_settings_load_from_env(monkeypatch): monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://u:p@host/db") monkeypatch.setenv("OLLAMA_BASE_URL", "http://10.30.20.107:11434") monkeypatch.setenv("SESSION_SECRET", "test-secret") settings = Settings(_env_file=None) assert settings.database_url == "postgresql+asyncpg://u:p@host/db" assert settings.ollama_base_url == "http://10.30.20.107:11434" assert settings.port == 7777 ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd backend && source venv/bin/activate && python -m pytest tests/test_config.py -v` Expected: FAIL with `ModuleNotFoundError: No module named 'app.config'` - [ ] **Step 3: Write minimal implementation** `backend/app/config.py`: ```python from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") database_url: str ollama_base_url: str session_secret: str port: int = 7777 settings = Settings() ``` `backend/app/db.py`: ```python from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase from app.config import settings engine = create_async_engine(settings.database_url, echo=False) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) class Base(DeclarativeBase): pass async def get_db() -> AsyncSession: async with async_session_maker() as session: yield session ``` Note: `Settings()` (module-level, in `app.config`) requires `DATABASE_URL`/`OLLAMA_BASE_URL`/`SESSION_SECRET` to be set in the environment or `.env` at import time. Task 3 introduces the real Postgres instance those values point to; for this task, `test_config.py` only exercises `Settings` directly with explicit env vars and never imports `app.db`, so no live database is required yet. - [ ] **Step 4: Run test to verify it passes** Run: `cd backend && source venv/bin/activate && python -m pytest tests/test_config.py -v` Expected: PASS - [ ] **Step 5: Commit** ```bash git add backend/app/config.py backend/app/db.py backend/tests/test_config.py git commit -m "feat: add settings and async db engine" ``` --- ## Task 3: User Model & Registration Endpoint **Files:** - Create: `backend/app/models/__init__.py` - Create: `backend/app/models/user.py` - Create: `backend/app/security.py` - Create: `backend/app/schemas.py` - Create: `backend/app/routes/__init__.py` - Create: `backend/app/routes/auth.py` - Modify: `backend/app/main.py` - Create: `backend/tests/conftest.py` - Test: `backend/tests/test_auth.py` **Interfaces:** - Consumes: `Base`, `get_db` from `app.db` (Task 2). - Produces: `User` model (`id: uuid.UUID`, `username: str`, `password_hash: str`, `email: str | None`, `created_at: datetime`) in `app.models.user`; `hash_password(password: str) -> str` and `verify_password(password: str, password_hash: str) -> bool` in `app.security`; `RegisterRequest`, `UserOut` in `app.schemas`; `POST /auth/register` route. `conftest.py`'s `client` fixture and `_reset_db` fixture are reused by every later test file. - [ ] **Step 1: Install Postgres and create the dev + test databases** This is a one-time environment setup step for this CT (skip if already done): ```bash sudo apt-get update && sudo apt-get install -y postgresql sudo -u postgres psql -c "CREATE ROLE quantumancy WITH LOGIN PASSWORD 'quantumancy';" sudo -u postgres psql -c "CREATE DATABASE quantumancy OWNER quantumancy;" sudo -u postgres psql -c "CREATE DATABASE quantumancy_test OWNER quantumancy;" ``` Verify: `PGPASSWORD=quantumancy psql -h localhost -U quantumancy -d quantumancy_test -c '\conninfo'` should connect without error. Copy `.env.example` to `.env` in the repo root (`cp .env.example .env`) if not already present — `app.config.Settings` reads it via `env_file=".env"`, and `backend/app` runs with the repo root as its working directory when started via the systemd unit or `uvicorn` from the repo root. For running tests from inside `backend/`, export `DATABASE_URL` directly instead (see Step 2's run command) rather than relying on the `.env` file's relative path. **`quantumancy_test` is dropped and recreated by every test run (see `conftest.py` below). Never point `DATABASE_URL` at the `quantumancy_test` database from anything other than the test suite.** - [ ] **Step 2: Write the failing tests** `backend/tests/conftest.py`: ```python import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import async_sessionmaker from app.db import Base, engine, get_db from app.main import app TestSessionLocal = async_sessionmaker(engine, expire_on_commit=False) @pytest_asyncio.fixture(autouse=True) async def _reset_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) yield async def _override_get_db(): async with TestSessionLocal() as session: yield session app.dependency_overrides[get_db] = _override_get_db @pytest_asyncio.fixture async def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac ``` `backend/tests/test_auth.py`: ```python import pytest @pytest.mark.asyncio async def test_register_creates_user(client): response = await client.post( "/auth/register", json={"username": "medium1", "password": "spookyspooky"}, ) assert response.status_code == 201 body = response.json() assert body["username"] == "medium1" assert "id" in body assert "password" not in body @pytest.mark.asyncio async def test_register_duplicate_username_rejected(client): await client.post("/auth/register", json={"username": "medium1", "password": "spookyspooky"}) response = await client.post("/auth/register", json={"username": "medium1", "password": "anotherpass"}) assert response.status_code == 409 ``` - [ ] **Step 3: Run test to verify it fails** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_auth.py -v ``` Expected: FAIL — `ModuleNotFoundError: No module named 'app.models'` (or similar import error) This exact `DATABASE_URL=... OLLAMA_BASE_URL=... SESSION_SECRET=... python -m pytest ...` invocation (pointed at `quantumancy_test`) is the standard way to run the test suite for the rest of this plan and all later plans. - [ ] **Step 4: Write minimal implementation** `backend/app/models/user.py`: ```python import uuid from datetime import datetime, timezone from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column from app.db import Base class User(Base): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) username: Mapped[str] = mapped_column(String(32), unique=True, index=True) password_hash: Mapped[str] = mapped_column(String(255)) email: Mapped[str | None] = mapped_column(String(255), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) ``` `backend/app/models/__init__.py`: ```python from app.models.user import User __all__ = ["User"] ``` `backend/app/security.py`: ```python from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError _hasher = PasswordHasher() def hash_password(password: str) -> str: return _hasher.hash(password) def verify_password(password: str, password_hash: str) -> bool: try: return _hasher.verify(password_hash, password) except VerifyMismatchError: return False ``` `backend/app/schemas.py`: ```python import uuid from pydantic import BaseModel, Field class RegisterRequest(BaseModel): username: str = Field(min_length=3, max_length=32) password: str = Field(min_length=8, max_length=128) email: str | None = None class UserOut(BaseModel): id: uuid.UUID username: str class Config: from_attributes = True ``` `backend/app/routes/__init__.py`: (empty file) `backend/app/routes/auth.py`: ```python from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_db from app.models.user import User from app.schemas import RegisterRequest, UserOut from app.security import hash_password router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED) async def register(payload: RegisterRequest, db: AsyncSession = Depends(get_db)): existing = await db.scalar(select(User).where(User.username == payload.username)) if existing is not None: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="username taken") user = User( username=payload.username, password_hash=hash_password(payload.password), email=payload.email, ) db.add(user) await db.commit() await db.refresh(user) return user ``` `backend/app/main.py` (replace entirely): ```python from contextlib import asynccontextmanager from fastapi import FastAPI import app.models # noqa: F401 — registers models on Base.metadata before create_all from app.db import Base, engine from app.routes.auth import router as auth_router @asynccontextmanager async def lifespan(app: FastAPI): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield app = FastAPI(title="Quantumancy", lifespan=lifespan) app.include_router(auth_router) @app.get("/healthz") async def healthz(): return {"status": "ok"} ``` - [ ] **Step 5: Run test to verify it passes** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_auth.py tests/test_health.py -v ``` Expected: PASS (all tests, including the Task 1 health check, which still needs to pass since `main.py` was rewritten) - [ ] **Step 6: Commit** ```bash git add backend/app/models backend/app/security.py backend/app/schemas.py backend/app/routes backend/app/main.py backend/tests/conftest.py backend/tests/test_auth.py git commit -m "feat: add user model and registration endpoint" ``` --- ## Task 4: Login, Session Cookie & get_current_user **Files:** - Create: `backend/app/models/auth_session.py` - Modify: `backend/app/models/__init__.py` - Create: `backend/app/deps.py` - Modify: `backend/app/schemas.py` - Modify: `backend/app/routes/auth.py` - Test: `backend/tests/test_auth.py` **Interfaces:** - Consumes: `User` (Task 3), `Base`/`get_db` (Task 2). - Produces: `AuthSession` model, `generate_session_token() -> tuple[str, str]` (raw token, sha256 hash), `hash_token(raw: str) -> str` in `app.models.auth_session`; `get_current_user(...) -> User` dependency and `SESSION_COOKIE_NAME` constant in `app.deps` — every later route needing an authenticated user depends on `get_current_user`. - [ ] **Step 1: Write the failing tests** Append to `backend/tests/test_auth.py`: ```python @pytest.mark.asyncio async def test_login_sets_cookie_and_me_returns_user(client): await client.post("/auth/register", json={"username": "medium2", "password": "spookyspooky"}) login_resp = await client.post("/auth/login", json={"username": "medium2", "password": "spookyspooky"}) assert login_resp.status_code == 200 assert "qm_session" in login_resp.cookies me_resp = await client.get("/auth/me") assert me_resp.status_code == 200 assert me_resp.json()["username"] == "medium2" @pytest.mark.asyncio async def test_login_wrong_password_rejected(client): await client.post("/auth/register", json={"username": "medium3", "password": "spookyspooky"}) response = await client.post("/auth/login", json={"username": "medium3", "password": "wrongpass"}) assert response.status_code == 401 @pytest.mark.asyncio async def test_me_without_cookie_rejected(client): response = await client.get("/auth/me") assert response.status_code == 401 ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_auth.py -v ``` Expected: FAIL — `404 Not Found` for `/auth/login` and `/auth/me` (routes don't exist yet) - [ ] **Step 3: Write minimal implementation** `backend/app/models/auth_session.py`: ```python import hashlib import secrets import uuid from datetime import datetime, timedelta, timezone from sqlalchemy import DateTime, ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column from app.db import Base SESSION_TTL = timedelta(days=14) class AuthSession(Base): __tablename__ = "auth_sessions" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id")) token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) def generate_session_token() -> tuple[str, str]: raw = secrets.token_urlsafe(32) return raw, hash_token(raw) def hash_token(raw: str) -> str: return hashlib.sha256(raw.encode()).hexdigest() ``` `backend/app/models/__init__.py` (replace entirely): ```python from app.models.auth_session import AuthSession from app.models.user import User __all__ = ["User", "AuthSession"] ``` `backend/app/deps.py`: ```python from datetime import datetime, timezone from fastapi import Cookie, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_db from app.models.auth_session import AuthSession, hash_token from app.models.user import User SESSION_COOKIE_NAME = "qm_session" async def get_current_user( qm_session: str | None = Cookie(default=None), db: AsyncSession = Depends(get_db), ) -> User: if qm_session is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "not authenticated") token_hash = hash_token(qm_session) result = await db.execute(select(AuthSession).where(AuthSession.token_hash == token_hash)) session = result.scalar_one_or_none() if session is None or session.expires_at < datetime.now(timezone.utc): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "session expired") user = await db.get(User, session.user_id) if user is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "user not found") return user ``` Append to `backend/app/schemas.py`: ```python class LoginRequest(BaseModel): username: str password: str ``` `backend/app/routes/auth.py` (replace entirely): ```python from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_db from app.deps import SESSION_COOKIE_NAME, get_current_user from app.models.auth_session import AuthSession, SESSION_TTL, generate_session_token from app.models.user import User from app.schemas import LoginRequest, RegisterRequest, UserOut from app.security import hash_password, verify_password router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED) async def register(payload: RegisterRequest, db: AsyncSession = Depends(get_db)): existing = await db.scalar(select(User).where(User.username == payload.username)) if existing is not None: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="username taken") user = User( username=payload.username, password_hash=hash_password(payload.password), email=payload.email, ) db.add(user) await db.commit() await db.refresh(user) return user @router.post("/login", response_model=UserOut) async def login(payload: LoginRequest, response: Response, db: AsyncSession = Depends(get_db)): user = await db.scalar(select(User).where(User.username == payload.username)) if user is None or not verify_password(payload.password, user.password_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials") raw_token, token_hash = generate_session_token() session = AuthSession( user_id=user.id, token_hash=token_hash, expires_at=datetime.now(timezone.utc) + SESSION_TTL, ) db.add(session) await db.commit() response.set_cookie( SESSION_COOKIE_NAME, raw_token, httponly=True, samesite="lax", max_age=int(SESSION_TTL.total_seconds()), ) return user @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) async def logout(response: Response): response.delete_cookie(SESSION_COOKIE_NAME) @router.get("/me", response_model=UserOut) async def me(user: User = Depends(get_current_user)): return user ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_auth.py -v ``` Expected: PASS - [ ] **Step 5: Commit** ```bash git add backend/app/models/auth_session.py backend/app/models/__init__.py backend/app/deps.py backend/app/schemas.py backend/app/routes/auth.py backend/tests/test_auth.py git commit -m "feat: add login, session cookies, and get_current_user" ``` --- ## Task 5: Rate Limiter Utility **Files:** - Create: `backend/app/rate_limit.py` - Test: `backend/tests/test_rate_limit.py` **Interfaces:** - Consumes: nothing (pure, standalone utility). - Produces: `RateLimiter(max_requests: int, window_seconds: float)` with `.allow(key: str) -> bool` in `app.rate_limit` — Plan 2 wires this onto the LLM-triggering endpoints per the spec's rate-limiting requirement. - [ ] **Step 1: Write the failing test** `backend/tests/test_rate_limit.py`: ```python from app.rate_limit import RateLimiter def test_allows_up_to_limit_then_blocks(): limiter = RateLimiter(max_requests=3, window_seconds=60) assert limiter.allow("user-1") is True assert limiter.allow("user-1") is True assert limiter.allow("user-1") is True assert limiter.allow("user-1") is False def test_different_keys_tracked_independently(): limiter = RateLimiter(max_requests=1, window_seconds=60) assert limiter.allow("user-1") is True assert limiter.allow("user-2") is True assert limiter.allow("user-1") is False ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_rate_limit.py -v ``` Expected: FAIL with `ModuleNotFoundError: No module named 'app.rate_limit'` - [ ] **Step 3: Write minimal implementation** `backend/app/rate_limit.py`: ```python import time from collections import defaultdict class RateLimiter: """Fixed-window limiter keyed by an arbitrary string (user id or client IP).""" def __init__(self, max_requests: int, window_seconds: float): self.max_requests = max_requests self.window_seconds = window_seconds self._hits: dict[str, list[float]] = defaultdict(list) def allow(self, key: str) -> bool: now = time.monotonic() window_start = now - self.window_seconds hits = self._hits[key] while hits and hits[0] < window_start: hits.pop(0) if len(hits) >= self.max_requests: return False hits.append(now) return True ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest tests/test_rate_limit.py -v ``` Expected: PASS - [ ] **Step 5: Run the full test suite** Run: ```bash cd backend && source venv/bin/activate DATABASE_URL=postgresql+asyncpg://quantumancy:quantumancy@localhost:5432/quantumancy_test \ OLLAMA_BASE_URL=http://10.30.20.107:11434 \ SESSION_SECRET=test-secret \ python -m pytest -v ``` Expected: PASS (all tests from Tasks 1-5) - [ ] **Step 6: Commit** ```bash git add backend/app/rate_limit.py backend/tests/test_rate_limit.py git commit -m "feat: add rate limiter utility" ``` --- ## Self-Review **Spec coverage:** repo scaffold (§2, §9) → Task 1. Postgres (§2), no-container/venv+systemd deployment (§2, §9) → Task 1 (systemd unit) and Task 3 (Postgres install). FastAPI backend (§2) → all tasks. Username/password + argon2 + open registration (§5) → Task 3. Server-side session cookies (§5) → Task 4. Per-account/per-IP rate limiting (§5) → Task 5 (utility only; enforcement on LLM endpoints is explicitly deferred to Plan 2, since those endpoints don't exist until then). Everything else in the spec (the four modes, Codex, TTS/i18n) is out of scope for Plan 1 by design — covered in Plans 2-7. **Placeholder scan:** none found — every step has complete, runnable code or exact commands. **Type consistency:** `User.id: uuid.UUID` matches `UserOut.id: uuid.UUID`. `generate_session_token() -> tuple[str, str]` return order (raw, hash) matches its usage in `routes/auth.py`'s `login` (`raw_token, token_hash = generate_session_token()`). `hash_token(raw: str) -> str` used identically in both `auth_session.py` (internally) and `deps.py`. `get_current_user` return type `User` matches its use as `user: User = Depends(get_current_user)` in `routes/auth.py` and is the exact name/import path (`app.deps.get_current_user`) later plans will depend on. --- Plan complete and saved to `docs/superpowers/plans/2026-07-20-quantumancy-01-foundation-auth.md`. Two execution options: **1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration **2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints **Which approach?**