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 two pillars; 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/scan 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, WorldModelCard) does
not collapse into Spectral’s platform vocabulary (ChangeSet, ScanTrace, Verdict).
| 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, rules, provenance tiers, World Agent, EvalSet corpus, WorldModelCard. |
spectral.platform | Platform — workspaces, evaluation frameworks, change sets, scans, optimization pipeline, Spectral Agent, Operations Agent. |
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.
| App | Language | Description |
|---|---|---|
apps/api | Python | FastAPI REST API; thin (auth + AgentTask dispatch + SSE streaming proxy) |
apps/workers | Python | Background job runners; hosts all three Spectral agent runtimes (Spectral / Ops / World) per ADR-060 |
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 | stdlib, pydantic | anything else |
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 rule 6 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 numbered validator rules referenced throughout this page (e.g. validator rule 7) are defined in
tools/quality/validate_architecture.py. See the file itself for the authoritative rule list.
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 │ ← Adapters: 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 scan pipeline testable without a real database or LLM.
- Infrastructure implements the protocols. Database repositories, LLM adapters, 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, eval corpus, world cards, provenance, World Agent memory)platform—spectral.platformtables (scans, change sets, evaluation results, scan traces, failure clusters, Spectral Agent memory, Operations Agent memory)
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.workspace_id')::uuid and similar — never
auth.uid() / auth.jwt(). The session-var contract lives in spectral.core.db.session_vars:
SESSION_VAR_ACCOUNT_ID = "app.account_id"(customer tenancy)SESSION_VAR_WORKSPACE_ID = "app.workspace_id"(workspace scope)SESSION_VAR_USER_ID = "app.user_id"(per-user identity, per ADR-059 D6)
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:
Account (tenant) └── Workspace ├── Workspace Agents ├── Change Sets ├── Traces ├── Samples / Sample Sets └── Evaluation Framework (workspace instance)Column strategy:
- Workspace-scoped tables: both
account_idandworkspace_id, both NOT NULL, indexed, with attached RLS policies from the first migration; plusdeleted_at timestamptz NULLper data retention — derived state via views. - Account-scoped tables (users, workspace membership, API keys):
account_idonly. - Platform-scoped tables (world model content, operator-only state): neither — platform role only, RLS-exempt by design.
For the full role model, the OPERATIONS scope taxonomy, and the deploy-tier key-exchange middleware, 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, provenance types |
application/ | WorldAgent, EvalSet generation pipeline, evolution loop, curation workflows |
infrastructure/ | Persistence for rules, eval corpus, holdout registry; embedding provider adapters |
spectral.platform — Optimization engine
Section titled “spectral.platform — Optimization engine”Everything related to how Spectral scans, scores, and recommends changes to customer agents.
| Layer | Key contents |
|---|---|
domain/ | ChangeSet, ScanTrace, EvalResult, FailureCluster, VerdictResult, workspace/agent primitives |
application/ | Seven-phase scan pipeline (observe → calibrate → diagnose → evaluate → optimize → safety → verdict), tournament, memory system, feedback |
infrastructure/ | Per-entity scanning repositories, LLM adapters, notification dispatchers, Spectral Agent persistence |
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 is organized by sub-directory, each with its own functional-area admission rule:
| 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; the audit procedure and responsible-owner posture inherit from ADR-067.
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/shared/protocols/, not concrete infrastructure:
# Protocol (application layer)@runtime_checkableclass LLMProvider(Protocol): def call(self, *, model: str, system: str, user: str, ...) -> LLMResponse: ... def call_with_tools(self, *, model: str, system: str, ...) -> ToolResponse: ...
# Concrete implementation (infrastructure layer)class PydanticAIAdapter: # implements LLMProvider def call(self, *, model, system, user, ...) -> LLMResponse: ...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: scan request lifecycle
Section titled “Data flow: scan request lifecycle”HTTP Request (customer hits /scans) → AuthMiddleware (verify JWT/API key, resolve workspace, compute scopes) → FastAPI Router (validate, extract dependencies from spectral.platform composition root) → Application Service in spectral.platform ├─ synchronously requests an EvalSet via a callee-owned OHS Protocol from │ spectral.worlds.contracts.protocols.* (per ADR-065 D3); the bridge tool composing │ the Protocol into the call site lives in apps/* per ADR-065 D5 ├─ runs seven-phase pipeline; each phase returns a Result[T] └─ emits scan-related events using producer-owned typed payloads in spectral.platform.contracts.events.* (per ADR-065 D2) onto the substrate → Worker subscriber in spectral.worlds parses the envelope into its own local <EventName>Event model (per ADR-065 D4) and routes through WorldAgent → API Router pattern-matches Result → HTTP response (RFC 9457 Problem Details on DomainError)The synchronous EvalSet request is the only Spectral → Worlds call-and-wait path. Everything else flows back to Worlds through events, which keeps Spectral’s request path independent of Worlds’ internal state.
Next steps
Section titled “Next steps”- Domain Model — entities, relationships, state machines
- Optimization Engine — the evaluation and scanning pipeline
- World Model System — the
spectral.worldsdesign - Agent Architecture — Spectral Agent, Operations Agent, WorldAgent
- 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)