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 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.


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, WorldModelCard) does not collapse into Spectral’s platform vocabulary (ChangeSet, ScanTrace, Verdict).

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, rules, provenance tiers, World Agent, EvalSet corpus, WorldModelCard.
spectral.platformPlatform — workspaces, evaluation frameworks, change sets, scans, optimization pipeline, Spectral Agent, Operations Agent.

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.

AppLanguageDescription
apps/apiPythonFastAPI REST API; thin (auth + AgentTask dispatch + SSE streaming proxy)
apps/workersPythonBackground job runners; hosts all three Spectral agent runtimes (Spectral / Ops / World) per ADR-060
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.corestdlib, pydanticanything else
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 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>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 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:

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 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.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, eval corpus, world cards, provenance, World Agent memory)
  • platformspectral.platform tables (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.

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_id and workspace_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.
  • Account-scoped tables (users, workspace membership, API keys): account_id only.
  • 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.


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, provenance types
application/WorldAgent, EvalSet generation pipeline, evolution loop, curation workflows
infrastructure/Persistence for rules, eval corpus, holdout registry; embedding provider adapters

Everything related to how Spectral scans, scores, and recommends changes to customer agents.

LayerKey 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 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-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; the audit procedure and responsible-owner posture inherit from ADR-067.


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_checkable
class 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).

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 (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.