Python REST API
This example shows a complete Contextia setup for a Python REST API built with FastAPI. It covers authentication and authorization, demonstrating how specs, decisions, norms, and code annotations work together.
Project overview
Section titled “Project overview”The project is an API backend for a task management application. It has user authentication (JWT-based), role-based authorization, and CRUD operations for tasks and projects.
taskflow-api/├── .contextia/│ ├── config.yaml│ └── system/│ ├── identity.md│ ├── specs/│ │ ├── SPEC-001.md # User authentication│ │ ├── SPEC-002.md # Role-based authorization│ │ └── SPEC-003.md # Task CRUD operations│ ├── rationale/│ │ ├── DEC-001.md # JWT over session cookies│ │ └── DEC-002.md # RBAC over ABAC│ └── norms/│ ├── NORM-SEC-001.md # Password handling│ └── NORM-API-001.md # Response format├── src/│ ├── auth/│ │ ├── router.py│ │ ├── service.py│ │ ├── models.py│ │ └── dependencies.py│ ├── tasks/│ │ ├── router.py│ │ ├── service.py│ │ └── models.py│ └── core/│ ├── config.py│ ├── security.py│ └── database.py└── tests/Configuration
Section titled “Configuration”project: name: taskflow-api languages: - python source_paths: - src/
annotations: prefix: "@" comment_syntax: python: "#"
check: ignore_patterns: - "tests/**" - "alembic/**"
context: max_tokens: 8000Identity document
Section titled “Identity document”---project: taskflow-apiversion: "1.0"---
## What is this
TaskFlow API is a REST backend for a collaborative task management platform.Built with FastAPI, SQLAlchemy, and PostgreSQL.
## Architecture
Single deployable service with domain modules: auth, tasks, projects.Each module has a router (HTTP layer), service (business logic), and models (data).Authentication uses JWT tokens. Authorization uses role-based access control.
## Conventions
- All endpoints return the standard envelope: { data, error, meta }.- Service functions raise domain exceptions; routers convert to HTTP responses.- Database access only through repository pattern in service layer.- All passwords hashed with bcrypt. Never store or log plaintext passwords.Authentication spec
Section titled “Authentication spec”---id: SPEC-001title: User authenticationdescription: JWT-based login, token refresh, and session validation.status: approveddecisions: - DEC-001norms: - NORM-SEC-001paths: - src/auth/router.py - src/auth/service.py - src/core/security.py---
## Objective
Provide stateless user authentication using JWT tokens, supportinglogin, token refresh, and logout.
## Behaviors
WHEN a user submits valid email and password to POST /auth/loginTHEN the system returns an access token (15min expiry) and a refresh token (7day expiry).
WHEN a user submits invalid credentials to POST /auth/loginTHEN the system returns HTTP 401 with error code "invalid_credentials".
WHEN a request includes a valid access token in the Authorization headerTHEN the system extracts the user identity and proceeds with the request.
WHEN a request includes an expired access tokenTHEN the system returns HTTP 401 with error code "token_expired".
WHEN a user submits a valid refresh token to POST /auth/refreshTHEN the system returns a new access token and rotates the refresh token.
WHEN a user calls POST /auth/logout with a valid tokenTHEN the refresh token is invalidated and cannot be reused.JWT decision record
Section titled “JWT decision record”---id: DEC-001title: JWT over session cookiesstatus: accepteddate: 2025-11-20specs: - SPEC-001---
## Context
Need stateless authentication for horizontal scaling behind a load balancer.Session store (Redis) adds operational complexity and a single point of failure.
## Decision
Use JWT (RS256) for access tokens. Short-lived (15 min) to limit exposurefrom stolen tokens. Refresh tokens stored in database for revocation capability.
## Alternatives considered
- **Session cookies + Redis**: Simpler token management but adds infrastructure dependency.- **Opaque tokens + database lookup**: Every request hits the database.
## Consequences
- No session store needed for access token validation.- Refresh tokens require database storage (acceptable, infrequent operation).- Token size is larger than session IDs (acceptable for API use).Security norm
Section titled “Security norm”---id: NORM-SEC-001title: Password handlingstatus: activepaths: - src/auth/** - src/core/security.py---
## Rules
1. Passwords MUST be hashed with bcrypt (cost factor 12) before storage.2. Plaintext passwords MUST NOT appear in logs, error messages, or API responses.3. Password comparison MUST use constant-time comparison to prevent timing attacks.4. Minimum password length is 8 characters. Enforce at the API validation layer.5. Failed login attempts MUST be rate-limited to 5 per minute per IP address.Annotated source code
Section titled “Annotated source code”Authentication router
Section titled “Authentication router”from fastapi import APIRouter, Depends, HTTPException
from src.auth.service import AuthServicefrom src.auth.models import LoginRequest, TokenResponsefrom src.core.security import get_current_user
router = APIRouter(prefix="/auth", tags=["auth"])
# @spec SPEC-001# @decision DEC-001@router.post("/login", response_model=TokenResponse)async def login(request: LoginRequest, auth: AuthService = Depends()): """Authenticate user and return JWT token pair.""" result = auth.authenticate(request.email, request.password) if result is None: raise HTTPException(status_code=401, detail="invalid_credentials") return result
# @spec SPEC-001@router.post("/refresh", response_model=TokenResponse)async def refresh_token(refresh_token: str, auth: AuthService = Depends()): """Exchange a valid refresh token for a new token pair.""" result = auth.refresh(refresh_token) if result is None: raise HTTPException(status_code=401, detail="invalid_refresh_token") return result
# @spec SPEC-001@router.post("/logout")async def logout(user=Depends(get_current_user), auth: AuthService = Depends()): """Invalidate the user's refresh token.""" auth.revoke_refresh_token(user.id) return {"data": None, "error": None, "meta": {}}Security utilities
Section titled “Security utilities”import bcryptfrom datetime import datetime, timedelta, timezonefrom jose import jwt
from src.core.config import settings
# @spec SPEC-001# @decision DEC-001# @spec SPEC-002def create_access_token(user_id: int, roles: list[str]) -> str: """Create a short-lived JWT access token with user roles.""" payload = { "sub": str(user_id), "roles": roles, "exp": datetime.now(timezone.utc) + timedelta(minutes=15), "type": "access", } return jwt.encode(payload, settings.jwt_private_key, algorithm="RS256")
# @norm NORM-SEC-001def hash_password(password: str) -> str: """Hash password with bcrypt. Never store plaintext.""" return bcrypt.hashpw( password.encode("utf-8"), bcrypt.gensalt(rounds=12), ).decode("utf-8")
# @norm NORM-SEC-001def verify_password(plain: str, hashed: str) -> bool: """Constant-time password comparison.""" return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))Running context assembly
Section titled “Running context assembly”When an agent works on a task related to authentication:
contextia context TASK-010Output:
── Task: TASK-010 — Add password reset flow ──specs: [SPEC-001]
── Spec: SPEC-001 — User authentication ──[Full spec with all WHEN/THEN behaviors]
── Decision: DEC-001 — JWT over session cookies ──[Decision with rationale and alternatives]
── Norm: NORM-SEC-001 — Password handling ──[All 5 security rules]The agent receives everything it needs to implement the password reset flow in a way that is consistent with existing patterns, respects security norms, and follows the JWT architecture.
Running integrity checks
Section titled “Running integrity checks”contextia checkChecking .contextia/ integrity... Specs: 3 found, 0 issues Decisions: 2 found, 0 issues Norms: 2 found, 0 issues Annotations: 8 found, 0 orphans, 0 missingAll checks passed.Next steps
Section titled “Next steps”- See the TypeScript Frontend example for a React application.
- Read Writing Effective Specs for spec quality guidelines.
- Check the Annotation Reference for complete syntax documentation.