A comprehensive account of the platform's architecture: the layered decomposition of
the backend, the component model of the frontend, the data pipeline, persistence
strategy, and API contract — grounded entirely in the production codebase.
Site Map
The platform is a single-page application with six distinct routes, a floating
chat widget accessible from every page, and eight individual vulnerability detail
pages branching from the home catalogue.
Platform Home
Vulnerability Catalogue
Mitigation Library
Testing Workbench
Prompt Enhancer
Security Knowledge Assistant
Chat Widget — persistent on all pages
Solid lines indicate direct navigation. The chat widget (dashed border) is mounted
in the root layout and is accessible from every route without navigation.
1. High-Level Architecture
The platform is structured as two independently deployable processes — a
React (Vite) frontend and a FastAPI backend — communicating
exclusively over an HTTP REST API with a single Server-Sent Events (SSE) channel for
streaming chat responses. This separation enforces a strict client/server boundary:
all business logic, model access, and persistence reside in the backend; the frontend
is a pure rendering and interaction layer with no direct database or LLM access.
External dependencies are confined to two integration points: the
model providers (connected through Microsoft Foundry AI or custom HTTP endpoints) (used by the platform model provider and the AI analysis
sub-system) and the Garak CLI (invoked as a managed subprocess for
red-teaming framework runs). Both integrations are behind abstract interfaces in the
domain layer, so neither affects the core business logic.
graph LR
subgraph Browser["Browser — React 18 · Vite · React Router v7"]
UI["Pages & Components"]
SSEClient["SSE Client"]
end
subgraph Backend["Backend — FastAPI · Python 3.12"]
API["API Layer"] --> APP["Application Layer"] --> DOMAIN["Domain Layer"] --> INFRA["Infrastructure Layer"]
end
DB[("SQLite")]
subgraph Ext["External Services"]
FOUNDRY["Azure AI Foundry (GPT · Claude · Llama · Phi · DeepSeek · Mistral)"]
EXTHTTP["External HTTP (user-provided)"]
GARAK["Garak CLI"]
end
UI -->|"REST JSON"| API
SSEClient -->|"SSE stream"| API
INFRA --> DB
INFRA --> FOUNDRY
INFRA --> EXTHTTP
INFRA --> GARAK
Deployment topology: both processes run on the same host during
development and are configured via environment variables (per-model
FOUNDRY_*_ENDPOINT and FOUNDRY_*_KEY pairs for each Azure
AI Foundry deployment, plus DATABASE_URL and
VITE_API_URL). No separate message broker, cache, or
authentication service is required.
2. Design Principles & Extensibility
The central architectural decision of this platform is that every major extension
point (adding a new LLM provider, a new test runner, or a new mitigation strategy)
is defined by a single-method abstract base class in the domain layer.
No routing logic, service code, or API layer needs to change. The entire system is
open for extension and closed for modification.
Three Core Interfaces
The domain layer defines exactly three abstract base classes. Together they cover every
way the platform interacts with external models and applies security controls.
LLM Integration
ModelProvider
classModelProvider(ABC):
@abstractmethodasync defgenerate(
self,
request: ModelRequest
) -> ModelResponse:
"""Execute a model call."""
pass
Because all business logic is written against these interfaces, and never against
concrete classes, every extension requires changes in exactly one place.
To add…
Implement
Register in
Services / API changed
A new LLM (e.g. Gemini)
GeminiProvider.generate()
MODEL_REGISTRY
None.ProviderRouter resolves it automatically.
A new test framework (e.g. PyRIT)
PyritRunner.run()
RunnerRouter
None. The API and TestService are unaware of runner types.
A new mitigation (e.g. semantic similarity guard)
NewMitigation.apply()
MITIGATION_REGISTRY
None. The runner pipeline applies all registered mitigations in layer order.
Open/Closed Principle in action: the platform shipped 10 LLM models,
12 mitigations, and 2 test runners without a single change to any service,
router, or API route. Each was added by implementing one method and inserting
one registry entry.
Supporting Architectural Patterns
Three further patterns work together to keep the interfaces clean and the system
testable end-to-end.
Layered Architecture
Dependencies always point inward. The domain layer — which owns all three ABCs —
has zero imports from FastAPI, SQLAlchemy, or any third-party library. Outer
layers depend on inner ones; the reverse never occurs.
Dependency Injection
A central Container wires all service objects at startup.
FastAPI route handlers receive services via Depends(), so no service
instantiates its own dependencies — every concrete class is substitutable
without touching call sites.
Repository Pattern
Persistence is mediated through TestRepository, which translates
between domain dataclasses and SQLAlchemy ORM models. Services call repository
methods; they are entirely unaware of the SQL dialect or table schema beneath.
Dependency Rule Visualised
An arrow from layer A to layer B means "A imports B". The domain layer —
where all three interfaces live — is at the centre and has no outward arrows.
graph LR
API["API Layer routes · schemas · FastAPI"]
APP["Application Layer TestService · ChatService ProviderRouter · RunnerRouter"]
DOMAIN["Domain Layer ModelProvider · TestRunner · Mitigation Test · ModelSpec · RunnerSpec"]
INFRA["Infrastructure Layer PlatformProvider · ExternalProvider PromptRunner · GarakRunner 12 Mitigation classes · TestRepository"]
API -->|"imports"| APP
APP -->|"imports"| DOMAIN
INFRA -->|"implements"| DOMAIN
API -.->|"depends on"| INFRA
3. Frontend Architecture
The frontend is a React 18 single-page application built with Vite and React Router v7.
It is organised around six top-level pages, each a dedicated TypeScript module under
app/pages/, backed by shared shadcn/ui components and a floating chat widget
accessible from every page. All LLM-facing and persistence operations are delegated to
the backend; the frontend holds no persistent state beyond React component state and
browser localStorage (chat history).
Real-time search and filter across all 8 vulnerability types; staggered card entrance animations
Test workbench
TestingPage.tsx
Multi-step form for model spec, environment, and runner; serialises to RunnerSpec/ModelSpec shapes and POSTs to /tests
Prompt enhancer
PromptEnhancerPage.tsx
Three-stage pipeline UI (improve → prepend mitigations → verify); side-by-side diff view of original vs. enhanced prompt
Chat assistant
SecurityKnowledgeAssistantPage.tsx + ChatWidget
Opens a text/event-stream SSE connection; accumulates streaming tokens into a React state buffer; persists history in localStorage
PDF export
lib/pdf/
Uses @react-pdf/renderer to compose a structured PDF report of test results entirely client-side
Component library
components/ui/
shadcn/ui components (buttons, dialogs, tables, tabs, toasts, etc.) providing a consistent Radix UI primitive foundation with dark-mode support
Shared TypeScript types serve as the interface contract between frontend and backend.
Every REST response is typed against these definitions, ensuring that any backend schema
change propagates as a compile-time type error rather than a silent runtime failure.
4. Backend Layered Architecture
The backend is decomposed into four layers. Each layer is a distinct Python package
under src/, and the dependency rule is enforced by import discipline:
no module in an inner package may import from an outer one.
graph LR
subgraph api["API Layer — src/api/"]
R["tests · knowledge · config chat · enhancements · health"]
end
subgraph app["Application Layer — src/app/"]
S["TestService · ProviderRouter RunnerRouter · ChatService"]
end
subgraph domain["Domain Layer — src/domain/"]
E["Test · ModelSpec · RunnerSpec ModelProvider · TestRunner · Mitigation"]
end
subgraph infra["Infrastructure Layer — src/infra/"]
I["PlatformProvider · ExternalProvider PromptRunner · GarakRunner TestRepository · PromptTestAnalyser"]
end
api --> app --> domain
infra -->|"implements"| domain
api -.->|"depends on"| infra
No business logic; delegates immediately to application services
Application
src/app/
Orchestration of domain objects and infrastructure calls; transaction boundaries; provider/runner routing
No direct I/O; no SQL; must remain unit-testable without network access
Domain
src/domain/
Pure business entities (frozen dataclasses), value objects, abstract interfaces for extension points, domain enumerations
Zero third-party imports; no I/O of any kind; represents the stable core
Infrastructure
src/infra/
Concrete model providers, test runners, mitigation implementations, ORM models, repositories, analysis engine, Garak subprocess management
Implements domain interfaces; handles all side effects; swappable without changing business logic
Model and Mitigation Registries
The infrastructure layer exposes two module-level dictionaries that act as
service locators for pluggable strategies. MODEL_REGISTRY maps
model identifiers (e.g. "gpt-5.2") to ModelProvider
instances; MITIGATION_REGISTRY maps slug strings (e.g.
"pre_filter") to Mitigation instances. Both registries
are populated at module import time, making new providers and mitigations addable
without modifying any service code.
5. Database Architecture
The backend uses a single SQLite database file at sqlite:///./app.db,
accessed via SQLAlchemy ORM and initialised at startup with
Base.metadata.create_all(bind=engine). Persisted storage is deliberately
narrow: only user-generated artefacts are written to disk. The platform's security
knowledge base is held separately in backend code as in-memory registries.
tests
Stores executed tests.
Column
Type
Key
id
String
PK
name
String
model
JSON
environment
JSON
runner
JSON
created_at
DateTime
Purpose: stores complete executed test records.
JSON fields:model, environment, and runner store the nested test configuration.
test_configurations
Stores saved test presets.
Column
Type
Key
id
String
PK
name
String
model
JSON
environment
JSON
runner
JSON
created_at
DateTime
INDEX
Purpose: stores saved configurations before a test is run.
Difference from tests: same structure, but used for reusable presets rather than historical executions.
prompt_enhancements
Stores prompt enhancement runs.
Column
Type
Key
id
String
PK
original_prompt
String
improved_prompt
String
enhanced_prompt
String
selected_mitigation_ids
JSON
verification_data
JSON
model_id
String
attempts
Integer
created_at
DateTime
INDEX
Purpose: stores the full output of the prompt enhancement pipeline.
JSON fields:selected_mitigation_ids stores the chosen mitigation list and verification_data stores the verifier result.
JSON column strategy: the model,
environment, runner, selected_mitigation_ids,
and verification_data fields are stored as JSON columns instead of
being normalised into child tables. For tests, TestRepository.create()
serialises the nested dataclasses with dataclasses.asdict(), and
TestRepository._to_domain() rebuilds them on read, including enum
coercion for RunnerType and rehydration of saved message history into
Message objects. This keeps the relational schema small while leaving
nested-structure validation in the application layer.
There are no foreign keys in the schema because each stored record is designed to be
self-contained. A test row already includes its model, environment, and runner
payloads; a prompt enhancement row already includes its selected mitigations and
verification result. For a single-user SQLite-backed educational platform, this is a
pragmatic tradeoff: simpler CRUD operations, no joins for common reads, and a domain
model that evolves more quickly than the SQL schema.
How Vulnerabilities and Mitigations Are Stored
The vulnerability catalogue and mitigation knowledge are not database
tables. They are stored in the backend as module-level Python dictionaries
populated with immutable dataclass instances when the process starts.
Backend source
Stored as
Role in the system
Persistence model
domain/knowledge/vulnerabilities.py
VULNERABILITIES: Dict[str, Vulnerability]
Canonical backend catalogue used by the Security Knowledge Assistant and keyword search
Static, version-controlled Python registry rebuilt at every backend startup
domain/knowledge/mitigations.py
MITIGATIONS: Dict[str, Mitigation]
Knowledge-base mitigation entries used for retrieval and explanatory chat responses
Static in-memory registry; not persisted to SQLite
infra/config/mitigations.py
MITIGATION_REGISTRY: Dict[str, MitigationConfig]
Executable mitigation configuration for prompt compilation and runner-layer defence application
Static runtime registry containing both prompt-layer messages and concrete mitigation implementations
This split is important. SQLite stores operational artefacts created by users during
normal use of the platform. By contrast, vulnerabilities and mitigations are shipped
with the codebase as trusted reference content. The chat assistant queries these
in-memory registries through search_knowledge_base(), which tokenises
and ranks the Python-defined entries directly without any SQL query, migration, or
seed data step.
The overall database architecture is therefore hybrid in practice:
relational persistence for mutable workflow state plus
static code-defined registries for security knowledge. Both are part
of backend state management, but only the former is managed by SQLAlchemy and written
to the SQLite file on disk.
6. API Design
The backend exposes a RESTful HTTP API mounted under /api/v1 with six
route modules. All request and response bodies are JSON validated by Pydantic v2
schemas. The API contract is formally documented by FastAPI's auto-generated OpenAPI
specification, accessible at /docs during development.
Route module
Prefix
Key endpoints
tests.py
/tests
GET / — list all tests POST / — create test GET /{id} — retrieve test PUT /{id} — update test DELETE /{id} — delete test POST /{id}/run — execute test and return RunResult
knowledge.py
/knowledge
GET /vulnerabilities — full vulnerability KB GET /mitigations — mitigation KB with MitigationLayer metadata GET /search — BM25-weighted field search across both KBs
config.py
/config
GET /models — available platform model identifiers GET /mitigations — registered mitigation slugs and metadata
chat.py
/chat
POST /stream — initiates an SSE stream; emits token-by-token
assistant response via text/event-stream
enhancements.py
/enhancements
GET / — list saved prompt enhancements POST / — store a new enhanced prompt
health.py
/health
GET / — liveness probe returning {"status": "ok"}
Request / Response Contract
The POST /tests/{id}/run endpoint is the most structurally complex,
returning a RunResult object that varies by runner type:
Relative URL to the Garak-generated HTML report file, served statically
The SSE endpoint at POST /chat/stream emits newline-delimited
data: events. Each event carries a JSON object with a single
token field. A terminal data: [DONE] event signals
end-of-stream, following the SSE streaming convention standardised by Azure AI Foundry's compatible endpoints.
7. Robust Error Handling
Robustness is handled as a first-class architectural concern. The backend distinguishes
between invalid user configuration, unknown mitigation setup, missing resources, and
failures from external systems such as model providers or framework subprocesses.
Instead of allowing these failures to crash the platform silently, each class of error
is caught close to its source, converted into a controlled response, and surfaced back
to the frontend with a clear failure reason.
Failure source
How it is caught
Backend response
Frontend effect
Invalid model, runner, or environment configuration
InvalidModelConfiguration raised in domain/service validation
HTTP 400 with the validation message in detail
The UI can show the user exactly which part of the request is invalid and prevent re-running until fixed
Custom or unknown mitigation IDs
UnknownMitigation is raised during prompt compilation or mitigation-layer lookup, then translated into InvalidModelConfiguration
HTTP 400 rather than an internal crash
The user sees that the mitigation setup is unsupported or malformed instead of receiving a generic failure
Missing test or configuration records
NotFoundError raised by the service layer
HTTP 404 with a specific not-found message
The frontend can tell the user the selected record no longer exists
External provider failures, HTTP errors, or Garak runtime failures
Provider and runner layers raise RuntimeError after capturing upstream failure details
HTTP 502 with the upstream error text in detail
The request returns in a controlled way, allowing the UI to show that the failure came from an external dependency rather than the browser or frontend
Prompt enhancement verification failure
EnhancementValidationError raised after the retry budget is exhausted
HTTP 422 with an explicit explanation that validation did not pass
The user is told that enhancement failed verification, rather than incorrectly being shown a "successful" result
Chat streaming failure mid-response
The SSE generator catches the exception while streaming
A final SSE delta is emitted with an error message, followed by a clean done event
The chat widget degrades gracefully instead of hanging indefinitely on a broken stream
Why this is robust: the platform does not hide failures behind vague
"something went wrong" behaviour. Validation errors are returned as client errors,
upstream provider and framework failures are returned as gateway-style errors, and
streamed chat failures are terminated cleanly. This means the frontend receives a
usable response body for failed requests and can present users with a concrete reason
for what is failing.
This is especially important for external integrations. When an OpenAI-compatible
endpoint, Anthropic call, Azure content filter, custom external HTTP model, or Garak
framework scan fails, the backend captures the upstream failure and returns it through
the API instead of swallowing it. The result is operational transparency: users can
distinguish a bad prompt, a broken mitigation configuration, a missing record, and an
external-system outage because each travels back through the request path with a
different status code and message.