Skip to content
GitHub
Foundations

Architecture

Spectral’s architecture serves three readers with different stakes in it:

  • Engineers building in the codebase need the import boundaries, the layer rules, and the data-flow paths.
  • Founders / strategic readers need the structural argument — why the topology buys authority isolation between the standard customers cite and the system being measured against it, and why customer-data isolation, evolvability, and clean-context boundaries are the load- bearing properties.
  • Ops need the deployment topology, the schemas, and the multi-tenancy enforcement model.

This page is the technical reference for all three. The strategic argument that motivates the three-context split lives in How Spectral Works — Why Spectral decides, not executes; this page assumes that argument and walks the structural mechanics.

The Spectral codebase is a single-library Python package (per ADR-031) plus per-app leaf namespaces. The library is partitioned into spectral.core, spectral.worlds, and spectral.platform — three contexts with different ubiquitous languages, communicating across narrow seams.

This page covers the code organization end-to-end: monorepo layout, the three-context split, Clean Architecture layers within each context, framework-layer composition, storage and isolation, and the request/decision data flow.


Spectral is a modular monorepo spanning Python, TypeScript, and SQL. The Python library + apps are managed by uv workspaces; TypeScript packages by pnpm workspaces. Turborepo orchestrates cross-language tasks (build, test, lint) with dependency-aware caching.

Two domain partitions plus one shared substrate in a single Python package at src/spectral/, with apps as separate snake-case leaf packages. The split is driven by genuinely different ubiquitous languages: world-model vocabulary (Rule, Provenance, World Agent, World Model Card, Action Module) does not collapse into Spectral’s platform vocabulary (Decision, Audit Chain, System Card, Override-pattern signal).

ModulePurpose
spectral.coreCode with no clean home in worlds or platform — substrate transport (events), cross-cutting plumbing (auth, db, retention, llm, embeddings, tools). Admission discipline in ADR-065.
spectral.worldsWorld Model System — world models (context schema, configuration, action registry), rules with applies_when + predicate, two-dimensional provenance, World Agent (code-generation + reflection), action modules, World Model Card.
spectral.platformPlatform (decision host) — decision API + MCP surface, decision-server, module store, audit chain, Customer Dashboard, System Card.

Apps are framework-layer leaves at apps/*/src/spectral_<app>/. Apps import from spectral.* and do not import from other apps. The workers entrypoint is the framework-layer composition seam (the per-app composition root that wires bridge tools and Tier 2 Protocols into the agent runtimes via DI at startup) per Contract Surfaces.

The workers tier runs in the single Cloudflare app container per the collapsed hosting topology in ADR-109 (one Cloudflare app container, with predicate execution in-process). The composition seam role and isolation from the decision hot path are unchanged.

AppLanguageDescription
apps/apiPythonFastAPI REST API; thin (auth + AgentTask dispatch + SSE streaming proxy)
apps/workersPythonWorkers tier (runs in the Cloudflare app container per ADR-109); hosts the World Agent runtime per ADR-060 and ADR-078
apps/dashboardTypeScriptCustomer-facing dashboard (TanStack Start; SPA)
apps/operationsTypeScriptStaff console (TanStack Start; SPA; Pattern A JWKS-local middleware)
apps/test-agentsPythonReference implementations for exploration + demonstration (see Test Agents)
apps/docs-codexTypeScriptThis documentation site (Astro/Starlight) — Cloudflare Pages + Pages Function for staff auth
apps/docs-userTypeScriptUser-facing documentation (Astro/Starlight) — Cloudflare Pages

apps/dashboard and apps/operations use TanStack Start (Vite + Router + Query + Form) — SPA mode today, SSR-per-route reserved. @supabase/supabase-js for auth (getClaims()); never used for data. The data path is Frontend → FastAPI → Postgres per ADR-034. Cookie scope is runspectral.com eTLD+1 — cross-subdomain session sharing across app., ops., and the staff codex. Pages Function. Full topology in frontend architecture. The Codex documentation sites (Astro/Starlight) deploy to Cloudflare Pages, not to the Start services.

DirectoryPurpose
tools/dev/Developer workflow (setup, start, precheck)
tools/quality/Quality gates: architecture validator, migration compat + naming lints, kernel-audit-candidate trigger, deploy-manifest coverage, response-model check, dependabot coverage, namespace-init guard, pnpm release-age guard, LLM SDK allowlist
tools/ops/Operational scripts (backup-nightly, premerge dry-run)
tools/provision/setup.sh provisioning orchestrator (per secrets management)
packages/Shared TS workspace packages: @spectral/design-tokens (CSS tokens + assets), @spectral/docs-core (Starlight sidebar + theme + components reused by docs-codex + docs-user)
supabase/Migrations, local config
docs/decisions/ADRs — browseable in Codex under the Decisions sidebar group
docs/runbooks/Operational runbooks — DR, hosting, edge, deployment, secrets, CI secrets, retention, testing

Dependency rules, enforced by tools/quality/validate_architecture.py with STRICT=True (violations block CI):

SourceMay importMay NOT import
spectral.corepure zone (everything under core/ not in an infrastructure/ sub-directory)stdlib, pydantic, spectral.coreinfrastructure SDKs, the infra zone (core/<area>/infrastructure/), any context
spectral.coreinfra zone (core/<area>/infrastructure/)stdlib, pydantic, spectral.core, infrastructure SDKs (fastembed, psycopg, httpx, litellm, …)any bounded context, any context’s domain types
spectral.worldsspectral.core, stdlib, pydanticspectral.platform (any layer, including platform.contracts.* — bridge tools live in apps/*, not in spectral.worlds or spectral.platform code)
spectral.platformspectral.core, stdlib, pydanticspectral.worlds (any layer, including worlds.contracts.*)
apps/*spectral.* (including <context>.contracts.*)another apps/*
tests/contracts/both producer and consumer typed payloads (validator tests-contracts-exempt exemption)n/a — bilateral tests need both sides of the contract

Communication between worlds and platform splits along flow shape per Contract Surfaces and ADR-063:

  • Notification flow → events via the event substrate; producer-owned typed payloads in <producer>.contracts.events.* per ADR-065 D2 (the catalog at Events). Consumers parse via their own local <EventName>Event model per ADR-065 D4.
  • Call flow → callee-owned OHS Protocol in <callee>.contracts.protocols.* per ADR-065 D3 (the catalog at Protocols); concrete impl lives in the callee’s application layer; per ADR-065 D5 bridge tools live in apps/* framework deliverables, never in caller-side library code.

No SQL grants between worlds and platform at any layer. The architecture validator carries an invariant AST rule flagging any direct SQL access between contexts from outside framework-layer composition entrypoints.

The validator rules referenced throughout this page (e.g. the app-context-surface rule) are enforced by tools/quality/validate_architecture.py. The authoritative registry — every rule’s stable slug, what it enforces, and its governing ADR — lives in ADR-097.


Clean Architecture inside worlds and platform

Section titled “Clean Architecture inside worlds and platform”

Clean Architecture applies inside each of spectral.worlds and spectral.platform, not between them. Both are layered the same way:

┌─────────────────────────────────────────────┐
│ apps/api / apps/workers │ ← Presentation: HTTP, background jobs
├─────────────────────────────────────────────┤
│ {context}/infrastructure │ ← Infrastructure impls: DB, LLM, auth, notifications
├─────────────────────────────────────────────┤
│ {context}/application │ ← Use cases: orchestration via protocols
├─────────────────────────────────────────────┤
│ {context}/domain │ ← Business rules: entities, value objects
└─────────────────────────────────────────────┘

Import rules inside spectral.worlds and spectral.platform — each layer may only import from layers below it:

LayerMay importMay NOT import
Domainstdlib, pydantic, spectral.coreapplication, infrastructure, apps
Applicationdomain, spectral.coreinfrastructure, apps
Infrastructuredomain, application, spectral.coreapps
APIthe same context’s domain / application / infrastructure, spectral.corethe other context, workers
Workersthe same context’s domain / application / infrastructure, spectral.corethe other context, api
  • Domain is pure business logic. No I/O, no framework imports. You can change the database or LLM provider without touching domain code.
  • Application orchestrates through protocols. Use cases depend on abstractions, not concrete infrastructure. This makes the decision execution pipeline testable without a real database or LLM.
  • Infrastructure implements the protocols. Database repositories, LLM provider implementations, notification senders — all concrete implementations live here.
  • API and Workers are deployment targets. They wire everything together but cannot import each other. They coordinate through database state and domain events.
  • Imports between worlds and platform are forbidden entirely. The two communicate through spectral.core types and events. The architecture validator catches violations; governance in ADR-065 catches additions to spectral.core that belong in worlds or platform rather than the shared substrate.

Three application schemas live inside one Supabase Postgres database (per ADR-032):

  • core — substrate contract tables (event outbox, deployments registry, retention registry, embedding profile, user mirror, shared lookups)
  • worldsspectral.worlds tables (rules, rule candidates, world models, action modules, source materials, distillation runs, World Model Cards, two-dimensional provenance, World Agent memory, operator-action audit)
  • platformspectral.platform tables (decision records, audit chain entries, override-pattern signals, System Card snapshots, conversation persistence)

public is reserved for Supabase-managed content — no app tables in public. auth, storage, realtime, langgraph, extensions are framework-owned (Supabase + LangGraph). Each context has its own scoped Postgres role: spectral_core_app, spectral_worlds_app, spectral_platform_app. These per-context roles are the basis for the multi-tenancy enforcement strategy below.

App-layer tenancy filtering is the primary enforcement boundary; RLS is the backstop. Per ADR-033, application-layer queries against tenant-scoped tables compose tenant predicates from the middleware-resolved AuthContext; the typed query helper that lands with the tenancy-enforcement epic codifies this pattern and exposes the validator hook that flags raw psycopg construction in application code.

RLS policies reference current_setting('app.domain_id')::uuid and similar — never auth.uid() / auth.jwt(). The session-var contract lives in spectral.core.db.session_vars:

  • SESSION_VAR_ORG_ID = "app.org_id" (customer org tenancy per ADR-086 D1)
  • SESSION_VAR_DOMAIN_ID = "app.domain_id" (domain scope per ADR-086 D2 + D6)
  • SESSION_VAR_USER_ID = "app.user_id" (per-user identity, per ADR-101 D3)

Connection-pool checkout sets the session vars via SET LOCAL inside an explicit transaction (per connection pooling) — not at pool checkout, since Supavisor transaction mode multiplexes physical backends per transaction.

Data ownership hierarchy:

Org (tenant)
└── Domain
├── World Model versions (context schema · configuration · action registry)
├── Action modules (per (org, domain, action, version))
├── Decisions + audit-chain entries
└── System Card (deployment-scoped)

Column strategy:

  • Domain-scoped tables: both org_id and domain_id, both NOT NULL, indexed, with attached RLS policies from the first migration; plus deleted_at timestamptz NULL per data retention — derived state via views.
  • Org-scoped tables (users, domain membership, API keys): org_id only.
  • Operations-scoped tables (world model content, operator-only state): neither — operations role only, RLS-exempt by design.

For the full role model and the OPERATIONS scope taxonomy, see Access Control.


Everything related to how world models are built, evolved, and audited. See the World Model System subtree for the full design.

LayerKey contents
domain/Rule, RuleCandidate, WorldModel, WorldModelCard, ActionModule, SourceMaterial, DistillationRun, two-dimensional provenance types
application/WorldAgent (code generation + applies_when generation + reflection), evolution loop, distillation pipeline, publication transaction
infrastructure/Persistence for rules, source materials, module store; DB-coupled embedding resolver + retriever implementations (the bare embedding provider lives in the core.embeddings.infrastructure zone per ADR-099)

Everything related to how Spectral receives /decide invocations, routes them to the deployed action modules, executes the modules, and writes the audit chain.

LayerKey contents
domain/Decision, AuditChainEntry, OverridePatternSignal, SystemCard, decision-context value types, org/domain primitives
application/Decision-execution Phases 2–5 (action map + module load → context establishment → predicate execution → aggregation + response) — Phase 1 (auth) runs upstream in AuthMiddleware; audit-chain projection; override-pattern signal aggregation
infrastructure/Decision-server implementations, module-store reader, audit-chain repository, notification dispatchers (the LLM control-plane implementations live in the core.llm.infrastructure zone per ADR-099)

spectral.core holds code with no clean home in worlds or platform per ADR-065. It is not a positive contract layer; an item belongs here iff the killer test (“if spectral.core did not exist, where would this go?”) has no clean single-context answer.

The kernel splits into two zones per ADR-099. The pure zone — everything under core/ not in an infrastructure/ sub-directory — is the contract surface: stdlib + pydantic + spectral.core only, no infrastructure SDKs, and may not import the infra zone. The infra zonecore/<area>/infrastructure/ — holds context-agnostic concrete implementations of that area’s contracts and may import infrastructure SDKs (fastembed, psycopg, litellm, …), but no bounded context and no context’s domain types. Because a consumer imports a core contract from the pure zone (core.embeddings.protocol, never core.embeddings.infrastructure) and the pure zone cannot reach the infra zone, importing a contract never transitively drags an SDK into the consumer.

The kernel is organized by sub-directory, each with its own functional-area admission rule. Each area MAY carry an infrastructure/ sub-directory holding context-agnostic concrete implementations of that area’s contracts — for example the FastEmbed embedding provider (core.embeddings.infrastructure), the outbox listener (core.events.infrastructure), and the LLM provider / router (core.llm.infrastructure):

Sub-directoryFunctional area
core/events/Substrate transport — EventEnvelope, publisher/listener Protocols, retry/status primitives
core/auth/Cross-cutting auth machinery — context payload, role enums, scope constants, error taxonomy
core/db/DB-shape constants pinned by RLS contract tests — schema names, session-var names
core/retention/Cross-cutting retention shapes — RetentionPolicy, registry, state enums
core/llm/LLM SDK abstraction + cross-cutting LLM value types
core/embeddings/Embedding SDK abstraction + cross-cutting embedding value types
core/tools/Agent-tool conventions per ADR-060ToolError taxonomy, metadata, approval payload

Contracts between worlds and platform are NOT in core. Per ADR-065 D2 + D3, producer-owned typed event payloads live in <context>.contracts.events.*; callee-owned OHS Protocols live in <context>.contracts.protocols.*. See Events and Protocols for the catalog; the mechanism-selection doctrine itself lives in Contract Surfaces.

Admission discipline (ADR-065 D1) is structural rather than review-shaped: per-sub-directory type-shape constraints are AST-checked by the architecture validator; admission-rationale docstrings are enforced via ruff D100 + D104; method-arg discipline (per ADR-068) keeps kernel value-type methods pure functions of self + value-typed args. The structural backstop for cumulative drift is the trigger-driven kernel re-audit per ADR-069, which carries the audit-candidate signals, the re-audit procedure, and the responsible-owner posture.


Application services inside spectral.worlds and spectral.platform depend on protocols defined in their own application/domain port layer (e.g. <context>/domain/.../ports.py), not concrete infrastructure:

# Protocol (application/domain port)
@runtime_checkable
class SourceMaterialRepository(Protocol):
async def get_source_material(self, *, source_material_id: UUID) -> SourceMaterial | None: ...
async def ingest(self, material: SourceMaterial) -> None: ...
# Concrete implementation (context infrastructure layer)
class PsycopgSourceMaterialRepository: # implements SourceMaterialRepository
async def get_source_material(self, *, source_material_id: UUID) -> SourceMaterial | None: ...

A context-agnostic concrete implementation of a kernel contract — one used by more than one context, with no context’s domain coupling — instead lives in the core infra zone (core/<area>/infrastructure/) per ADR-099, not in either context’s infrastructure layer. The FastEmbed EmbeddingProvider, the outbox listener, and the model-profile router are the worked examples.

OHS Protocols are admitted under ADR-065 D3 as the synchronous-call surface — they live in <callee>.contracts.protocols.* (callee-owned) and are consumed by apps/* framework deliverables only (per ADR-065 D5). Caller-side library code never imports the other context’s contracts; bridge tools live at the framework layer (see Protocols for the catalog; WorldAgentRunner is the worked reference example).

Application services return Result[T] instead of raising exceptions for expected business failures:

type Result[T] = Success[T] | Failure
@dataclass(frozen=True)
class Success[T]:
value: T
@dataclass(frozen=True)
class Failure:
error: DomainError

The API layer pattern-matches on the result to produce the appropriate HTTP response. Programming errors (invariant violations) still raise exceptions.

Each deployment target has its own composition root. There is no composition between worlds and platform inside the library — each wires its own graph, and the deployment target combines them. Today both deployment apps ship as scaffolds; the composition seams below are the locked target shape and land per the API and workers epics:

  • apps/api/src/spectral_api/dependencies.py — API composition root; receives services from worlds and platform and exposes them to routers via Depends()
  • apps/workers/src/spectral_workers/dependencies.py — workers composition; subscribes event handlers per context
  • Each of spectral.worlds and spectral.platform ships its own helper composition module (e.g. spectral.platform.infrastructure.intelligence.composition) so the deployment roots can import a pre-wired context-level service rather than re-assembling internals

HTTP Request (caller hits /decide)
→ AuthMiddleware — Phase 1 (verify JWT/API key per ADR-087, resolve (org_id, domain_id), compute scopes)
→ FastAPI Router (validate request body, extract dependencies from spectral.platform composition root)
→ Application Service in spectral.platform — decision-execution Phases 2–5 per ADR-076:
├─ Phase 2: routes to deployed action module via the module-store reader;
│ module-store entries are written by spectral.worlds at publication time
│ (worlds → platform handoff via content-addressed module store; not a runtime
│ cross-context call)
├─ Phases 3–5: validates supplied context against the embedded schema, runs derivations,
│ evaluates rule predicates in severity order, applies suppression chains, runs aggregation
└─ writes the audit-chain entry per ADR-076 D3 using producer-owned typed payloads in
spectral.platform.contracts.events.* (per ADR-065 D2) onto the substrate
→ API Router returns { status, work_frame, decision_metadata } per ADR-077 D3
(RFC 9457 Problem Details on transport-level errors)

The module store is the unidirectional handoff from worlds (authoring) to platform (execution). There is no runtime cross-context call on the decision path — every artifact the decision-server needs is in the module bundle the platform decision-server loaded from the store. Override-pattern signals from customer-flagged decisions flow back to worlds through events, which keeps the decision path independent of worlds’ internal state.