Skip to main content

Appendix M: The Canonical Technology Stack

This appendix is the curated three-tier list referenced from Section 4.5 of the main methodology document. Every S4U project's ADR-0001 references this list as its starting point and either accepts the canonical defaults or amends them with explicit rationale.

The stack is curated from the union of three S4U projects in production: Trust Relay (workflow + RAG, FastAPI/Next.js), Zol-RAG (voice + RAG, FastAPI/React+Vite + LiveKit — frontend deviates from baseline, see below), and Ratiba.chat (conversational SMB management, FastAPI/Next.js + WhatsApp + M-Pesa). Each entry below carries an "inspired-from" reference so the lineage is auditable.

Tier definitions

  • Mandatory. Always use this. Deviation requires a new ADR in the project's docs/adr/ justifying why this project departs from canon and what the alternative buys.
  • Default. Use this unless you have a project-specific reason to pick differently. Reasons go in the project's existing ADR-0001 as a "deviation rationale" entry. No new ADR needed.
  • Forbidden. Never use this regardless of project. No deviation path; using forbidden items breaks methodology compliance.

Promotion rules (per §4.5 rule 3): a library moves from default to mandatory after ≥2 projects ship with it for ≥3 months. A library moves to forbidden after ≥1 project hits a real failure caused by it. New additions land in default first.


Backend Runtime + Framework

Mandatory

ItemInspired-fromNotes
Python 3.13+Ratiba ADR-0001 (amended 2026-04-25)Pin in pyproject.toml::requires-python. Re-evaluate annually. Trust Relay + Zol-RAG should bump to 3.13 at next opportunity.
FastAPITrust Relay, Zol-RAG, Ratiba (all three)Async-first; matches the lifespan + dependency-injection patterns shared across all S4U projects.
Pydantic v2 with model_config = ConfigDict(extra="forbid") everywhereTrust Relay, Zol-RAG, RatibaThe extra="forbid" discipline catches schema drift at runtime. Used at every API boundary, every config object, every ORM-adjacent DTO.
Type hints + pyright for static type checkingTrust Relay (full coverage), Ratiba (full coverage post-M3)Pyright via the pyright-lsp plugin in Claude Code; pyright command-line in CI. Type errors are commit-blocking.

Default

ItemInspired-fromNotes
uvicorn for ASGI serverAll threeStandard FastAPI runtime; no compelling alternative.
python-dotenv via pydantic-settingsAll threeSettings class with env_file=".env"; production secrets injected via deploy infra, not .env.

Forbidden

  • Stdlib logging directly in application code. Mandatory replacement: structlog. See observability tier.
  • requests library for new HTTP code. Sync, not async-native; falls outside the FastAPI async story.

Data Layer

Mandatory

ItemInspired-fromNotes
PostgreSQL 16+Ratiba ADR-0001, Trust RelayThe single relational store across all S4U projects. Schema-per-tenant (Ratiba) or single-schema (Trust Relay) depending on multi-tenancy needs.
Alembic for migrationsAll threePer-tenant invocation pattern in Ratiba (per-tenant ADR-0002); single-database invocation in Trust Relay + Zol-RAG.
testcontainers for integration tests against real PostgresAll threeRequired for the no-mocking-by-default rule (see §7) to be enforceable. In-memory DB substitution is forbidden (below).

Default

ItemInspired-fromNotes
asyncpg for application Postgres queriesTrust Relay, RatibaFastest Python Postgres driver; native to the async stack.
SQLAlchemy 2.0 (async) for ORMTrust Relay, RatibaMature schema-per-tenant story (SET search_path per session).
psycopg 3 only when a library forces itRatiba ADR-0001 amendmentSpecifically for langgraph-checkpoint-postgres (see Ratiba M3 spike). Two-driver wart documented in ADR-0001 Consequences.
Redis 7 for cache + ephemeral stateAll threeSession state, rate limiting, webhook dedup, FSM hot state.
MinIO for object storage in devTrust Relay, RatibaS3-compatible; dev-time substitute for AWS S3.

Forbidden

  • In-memory databases (SQLite, etc.) as test fixtures for code that targets Postgres in production. The behaviour drift between SQLite and Postgres has bitten Trust Relay (mock/prod divergence in 2026-Q1). Always testcontainers.
  • The ::jsonb cast syntax with asyncpg. Use CAST(:param AS jsonb) (already in the global CLAUDE.md). Asyncpg's parameter parser breaks on ::.
  • Python-side default=... on SQLAlchemy ORM columns when the migration sets server_default. SQLAlchemy's compare_metadata() (used by Alembic autogenerate diffs in test_models_match_migration_metadata-style tests) only compares DB-side defaults; Python-side default=False reads as drift against migration server_default=text("FALSE"). ORM columns with non-NULL defaults must mirror the migration verbatim with server_default=text("..."). Codified after Ratiba M6 T11 — added in v2.1.2 (2026-04-30).
  • Releasing an asyncpg LISTEN connection back to the shared pool via pool.release(conn). Listener-state connections deadlock pool.close() on shutdown — the listener task holds a reference; the pool can't drain. Use await conn.close() directly in the listener task's finally instead of pool.release(conn) — the pool detects the dead conn and re-creates it. Codified after Ratiba M9 T1 — added in v2.1.3 (2026-05-05). Pin in production-LISTEN consumers AND in tests using pool.add_listener(...) (test-pollution under shared LISTEN connection has the same root cause; per-test fresh asyncpg connection avoids it).

Testing

Mandatory

ItemInspired-fromNotes
pytest with asyncio_mode = "auto"All threeStandard async test config.
testcontainers for any external service (Postgres, Keycloak, Redis, MinIO, etc.)All threeThe mechanism that makes "no mocking by default" enforceable.
No-mocking-by-default rule (§7)All threeMocking external APIs requires an explicit # MOCK APPROVED: comment with reason + approver + alternative-real-service path. Internal class mocking is forbidden (next tier).

Default

ItemInspired-fromNotes
respx for HTTP mocking when an external API is being mocked-with-approvalRatiba M4 (WhatsApp Sender), Zol-RAG (Deepgram + ElevenLabs)Mocks raw httpx; SDK transport mocking is more brittle.
freezegun for time-travel testsRatiba (M3 lifecycle, M4 idempotency TTL)Required for time.sleep() ban enforcement — fast tests of timing-dependent code.
pytest-xdist parallelization when full-suite wall-clock exceeds ~10 minRatiba M7+testcontainer-spinup amortizes well across parallel workers; cuts ~18min suite to ~5min. Codified in v2.1.3 (2026-05-05) after Ratiba M7+M9 plateau at 15-18min. Mandatory tier when wall-clock crosses 10min.

Forbidden

  • time.sleep() in tests. Mandatory replacement: asyncio.sleep() (when you genuinely need a yield) or freezegun (when you need time travel). Already in the global CLAUDE.md.
  • unittest.mock of internal classes. Mocks are for process boundaries (HTTP APIs, external services with approved mocks); never for first-party classes. The right test seam for first-party code is fixture injection.
  • mock.assert_awaited_once() followed by direct .await_args.kwargs access without an explicit assert mock.await_args is not None between them (v2.1.3). pyright sees await_args as Optional[Call] regardless of the assert_awaited_once contract. Pin the explicit narrowing assert.

Frontend

Mandatory

ItemInspired-fromNotes
Next.js 14+ App RouterTrust Relay, Ratiba (zol-rag deviates: React+Vite, pre-dates the baseline; deviation ADR required at project level — the three-copies-all-false drift this row carried is assessment finding CE-5)The S4U frontend baseline. App Router (not Pages Router) is the current standard.
TypeScript with strict modeAll threeNo .js files in new frontend code. tsconfig.json::strict: true.
Tailwind CSS v4 (CSS-first)Ratiba M9v4 forced by shadcn 4.x's @theme + oklch() syntax. v3 still works for legacy projects but new scaffolds get v4. Codified in v2.1.3 (2026-05-05).
shadcn/ui 4+ added per-component via npx shadcn@latest add <component>All threeNOT a direct dependency. Components are copied into the project so they can be customised. shadcn 4.x emits Tailwind v4 syntax — pairing forces the v4 entry above.
Sonner for toast notificationsAll threeThe mandatory replacement for alert() / native dialogs (forbidden tier).

Default

ItemInspired-fromNotes
next/font for font loadingTrust Relay, RatibaSelf-hosted; no Google Fonts CDN dependency.
Skeleton loaders (not spinners) for content loadingAll threePer S4U UI/UX rules.
next-auth@5.x (NextAuth v5) with Keycloak provider for SSORatiba M9v5 server-component auth() returns the session synchronously in Server Components; account.access_token captured into session.accessToken via JWT/session callbacks. Codified in v2.1.3 (2026-05-05).
vitest + @testing-library/react for component testsRatiba M9Vitest 4+ requires @testing-library/jest-dom/vitest subpath import (not the root) for matcher augmentation; needs vitest.setup.ts in setupFiles config.

Forbidden

  • alert(), confirm(), native browser dialogs for any user-facing flow. Mandatory replacement: inline confirmation UI + Sonner toasts (already in S4U UI/UX rules).
  • Modal dialogs for simple confirmations. Use inline UI or Sheet (per §UI/UX rules).
  • Pages Router for new Next.js projects. App Router is canonical.

Observability + Logging

Mandatory

ItemInspired-fromNotes
structlog for all application loggingTrust Relay, Zol-RAG, RatibaStdlib logging direct usage is forbidden in application code (a process-level handler that captures structlog and routes to stdlib for transport is fine — that's plumbing, not application code). Mandate enables the evidence-over-claims rule from §2.3 (structured logs are mechanically queryable; unstructured print() output is not).
structlog.testing.capture_logs() for log assertions in testsRatiba (codified during M5 T1 cost-ceiling tests)pytest's caplog only captures stdlib logging; structlog isn't routed through stdlib by default in S4U projects, so caplog silently fails to capture structlog events. Always use capture_logs().

Default

ItemInspired-fromNotes
Self-hosted observability (no SaaS by default)Ratiba ADR-0001 (no-cloud-dependencies bias)Project-specific deviation acceptable; document in ADR-0001.

Forbidden

  • Stdlib logging directly in application code. See mandatory tier.
  • Ad-hoc print() for logging. Always structlog.

Authentication + Identity

Mandatory

ItemInspired-fromNotes
Keycloak 24+ for production authenticationTrust Relay, RatibaSelf-hosted; no per-MAU SaaS cost. Tenant realms (Ratiba) or single realm (Trust Relay) per multi-tenancy shape.
OIDC for dashboard auth flowsAll threeKeycloak as the OIDC provider.

Default

ItemInspired-fromNotes
python-keycloak library for backend Keycloak admin APITrust Relay, RatibaThe maintained Python client. Note Ratiba M3's user_realm_name="master" decoupling pattern (Keycloak admin user lives in master realm; tenant realms are addressed by URL path).

Forbidden

  • Hardcoded admin keys committed to the repo. PoC artifacts allowed (e.g., Ratiba's ADMIN_API_KEY for /api/v1/admin/*) but must be replaced by Keycloak master-realm + role-based access by milestone X (specified per project). Never long-lived secrets in version control.

Infrastructure + Containers

Mandatory

ItemInspired-fromNotes
Docker Compose for dev + small-VPS prodAll threeProduction path to Kubernetes if scale demands it; not on any current S4U roadmap.
Per-project port assignment table in CLAUDE.mdAll threeAll three projects can run concurrently with no host-port conflicts. The table is shared in §4.3 / project CLAUDE.md.

Default

ItemInspired-fromNotes
pyproject.toml (PEP 621) for Python dependenciesAll threeNot requirements.txt.
pip install -e ".[dev]" for editable dev installsAll threeStandard pattern.

Forbidden

  • Manual non-versioned dependency installs in production. Pin everything; reproducible builds only.

Voice + Conversational (project-specific tiers)

These items are mandatory only for projects with voice or conversational AI scope. Stack-wide they are "default per scope."

Default (when in scope)

ItemInspired-fromNotes
Deepgram Nova-3 for STTZol-RAG, RatibaMultilingual; same key management across S4U.
ElevenLabs Multilingual v2 for TTSZol-RAG, RatibaReuse playbook from sister projects.
LiveKit (SIP + rooms) for telephonyZol-RAG, RatibaSIP bridge for phone-number ingress; rooms for the agent worker pattern.
langgraph + langgraph-checkpoint-postgres for FSM orchestrationRatiba M5Carries the MIGRATIONS workaround for CREATE INDEX CONCURRENTLY (Ratiba upstream issue #7630 at langchain-ai/langgraph).

Library Currency Policy

Per Ratiba ADR-0001 amendment (2026-04-25), the canonical stack is not a frozen list. Currency policy:

  1. At lock-in (initial pinning): every dependency is pinned to its latest stable release at the moment of pinning. No "match what sister-project N uses" — the PoC is the moment to start at the front.
  2. Patch releases (x.y.Z): auto-applied via Renovate / Dependabot.
  3. Minor releases (x.Y.0): reviewed monthly. Adopt unless something breaks.
  4. Major releases (X.0.0): reviewed quarterly. ADR-worthy for any major bump that changes a load-bearing contract.
  5. Deprecation tracking: subscribe to release notes for the load-bearing libraries (FastAPI, Pydantic, SQLAlchemy, structlog, Next.js, shadcn/ui).

Promotion / demotion of stack items between tiers happens by methodology amendment (this appendix is amended; methodology version bumps to vN.M+1).


Deviation Templates

Template 1: Mandatory deviation (new ADR)

Create docs/adr/ADR-NNNN-{deviation-name}.md:

# ADR-NNNN: Deviate from Canonical Stack — {item}

**Status:** Accepted
**Date:** YYYY-MM-DD

## Context

The canonical S4U methodology stack mandates {mandatory item} (see appendix-m). This project departs from the mandate because {project-specific reason that genuinely doesn't apply to other S4U projects}.

## Decision

Use {alternative} instead of {mandatory item} for {scope}.

## Consequences

**Positive.** {What the alternative buys}.
**Negative.** {What the deviation costs vs canonical: contributor onboarding cost, sister-project skill non-transfer, plumbing differences, etc.}.

## Alternatives Considered

| Alternative | Rejected because |
|---|---|
| Use the mandatory item | {Why it doesn't fit this project} |
| {Other alternative} | {Why} |

Template 2: Default deviation (ADR-0001 entry)

Add a section to the project's docs/adr/ADR-0001-tech-stack.md:

## Deviations from Canonical Stack

| Default item | Replaced with | Rationale |
|---|---|---|
| `httpx` | `aiohttp` | {Project-specific reason — e.g., aiohttp's WebSocket support is needed and httpx doesn't have it} |

No new ADR; the existing ADR-0001 carries the rationale.


Summary

The canonical stack is the methodology's answer to "what should I build with?" — three tiers, each with proportional deviation cost, curated from the union of three production S4U projects. Items move between tiers by amendment, not by per-PR judgment. The full list above is the source of truth; project ADR-0001s reference back rather than re-deriving.