Files
qtalker---/docs/superpowers/plans/2026-07-20-quantumancy-01-foundation-auth.md
Indiana 7ab8adce83 Rewrite Plan 1 to drop Docker: venv + systemd + apt Postgres
Task 1 now scaffolds a Python venv and a systemd unit template
instead of a Dockerfile/Compose stack. Task 3 installs Postgres
directly via apt and creates dev/test databases on the same
instance. All test-run commands switched from `docker compose run`
to plain `pytest` with exported env vars.
2026-07-20 07:04:39 +00:00

29 KiB

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:

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:

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

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

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:

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

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:

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:

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

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:

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:

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:

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:

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:

from app.models.user import User

__all__ = ["User"]

backend/app/security.py:

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:

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:

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

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:

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

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:

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

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:

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

from app.models.auth_session import AuthSession
from app.models.user import User

__all__ = ["User", "AuthSession"]

backend/app/deps.py:

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:

class LoginRequest(BaseModel):
    username: str
    password: str

backend/app/routes/auth.py (replace entirely):

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:

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

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:

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:

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:

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:

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