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.
Monorepo layout
Section titled “Monorepo layout”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.
worlds, platform, and core in one library
Section titled “worlds, platform, and core in one library”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).
| Module | Purpose |
|---|---|
spectral.core | Code 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.worlds | World 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.platform | Platform (decision host) — decision API + MCP surface, decision-server, module store, audit chain, Customer Dashboard, System Card. |
Apps (deployment targets)
Section titled “Apps (deployment targets)”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.
| App | Language | Description |
|---|---|---|
apps/api | Python | FastAPI REST API; thin (auth + AgentTask dispatch + SSE streaming proxy) |
apps/workers | Python | Workers tier (runs in the Cloudflare app container per ADR-109); hosts the World Agent runtime per ADR-060 and ADR-078 |
apps/dashboard | TypeScript | Customer-facing dashboard (TanStack Start; SPA) |
apps/operations | TypeScript | Staff console (TanStack Start; SPA; Pattern A JWKS-local middleware) |
apps/test-agents | Python | Reference implementations for exploration + demonstration (see Test Agents) |
apps/docs-codex | TypeScript | This documentation site (Astro/Starlight) — Cloudflare Pages + Pages Function for staff auth |
apps/docs-user | TypeScript | User-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.
Supporting directories
Section titled “Supporting directories”| Directory | Purpose |
|---|---|
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 |
Package dependency topology
Section titled “Package dependency topology”Dependency rules, enforced by tools/quality/validate_architecture.py with STRICT=True
(violations block CI):
| Source | May import | May NOT import |
|---|---|---|
spectral.core — pure zone (everything under core/ not in an infrastructure/ sub-directory) | stdlib, pydantic, spectral.core | infrastructure SDKs, the infra zone (core/<area>/infrastructure/), any context |
spectral.core — infra zone (core/<area>/infrastructure/) | stdlib, pydantic, spectral.core, infrastructure SDKs (fastembed, psycopg, httpx, litellm, …) | any bounded context, any context’s domain types |
spectral.worlds | spectral.core, stdlib, pydantic | spectral.platform (any layer, including platform.contracts.* — bridge tools live in apps/*, not in spectral.worlds or spectral.platform code) |
spectral.platform | spectral.core, stdlib, pydantic | spectral.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>Eventmodel 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 inapps/*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-surfacerule) are enforced bytools/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:
| Layer | May import | May NOT import |
|---|---|---|
| Domain | stdlib, pydantic, spectral.core | application, infrastructure, apps |
| Application | domain, spectral.core | infrastructure, apps |
| Infrastructure | domain, application, spectral.core | apps |
| API | the same context’s domain / application / infrastructure, spectral.core | the other context, workers |
| Workers | the same context’s domain / application / infrastructure, spectral.core | the other context, api |
Why these boundaries matter
Section titled “Why these boundaries matter”- 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.coretypes and events. The architecture validator catches violations; governance in ADR-065 catches additions tospectral.corethat belong in worlds or platform rather than the shared substrate.
Schemas
Section titled “Schemas”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)worlds—spectral.worldstables (rules, rule candidates, world models, action modules, source materials, distillation runs, World Model Cards, two-dimensional provenance, World Agent memory, operator-action audit)platform—spectral.platformtables (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.
Multi-tenancy isolation
Section titled “Multi-tenancy isolation”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_idanddomain_id, both NOT NULL, indexed, with attached RLS policies from the first migration; plusdeleted_at timestamptz NULLper data retention — derived state via views. - Org-scoped tables (users, domain membership, API keys):
org_idonly. - 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.
worlds, platform, and core at a glance
Section titled “worlds, platform, and core at a glance”spectral.worlds — World Model System
Section titled “spectral.worlds — World Model System”Everything related to how world models are built, evolved, and audited. See the World Model System subtree for the full design.
| Layer | Key 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) |
spectral.platform — Decision host
Section titled “spectral.platform — Decision host”Everything related to how Spectral receives /decide invocations, routes them to the
deployed action modules, executes the modules, and writes the audit chain.
| Layer | Key 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 — Shared substrate
Section titled “spectral.core — Shared substrate”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 zone — core/<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-directory | Functional 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-060 — ToolError 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.
Key patterns
Section titled “Key patterns”Protocol-driven dependency injection
Section titled “Protocol-driven dependency injection”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_checkableclass 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).
Result pattern
Section titled “Result pattern”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: DomainErrorThe API layer pattern-matches on the result to produce the appropriate HTTP response. Programming errors (invariant violations) still raise exceptions.
Composition roots
Section titled “Composition roots”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 viaDepends()apps/workers/src/spectral_workers/dependencies.py— workers composition; subscribes event handlers per context- Each of
spectral.worldsandspectral.platformships 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
Data flow: decision request lifecycle
Section titled “Data flow: decision request lifecycle”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.
Next steps
Section titled “Next steps”- Domain Model — entities, relationships, state machines
- Decision Execution — the five-phase breakdown of every
/decideinvocation - World Model System — the
spectral.worldsdesign - Agent Architecture — the World Agent
- Your First Feature — build a vertical slice end-to-end
docs/design/library-split-playbook.md— operational lineage of the worlds/platform/core monorepo split (per ADR-031)