Layer System
Deep dive into SystemPrompt's five-layer architecture
On this page
SystemPrompt organizes its 33 crates into five distinct layers. Each layer has specific responsibilities and dependency rules.
Layer Diagram
┌───────────────────────────────────────────────────────────────┐
│ ENTRY LAYER │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ api │ │ cli │ │
│ └──────┬──────┘ └──────┬──────┘ │
├─────────┴────────────────┴────────────────────────────────────┤
│ APPLICATION LAYER │
│ ┌─────────┐ ┌───────────┐ ┌───────────┐ ┌──────┐ │
│ │ runtime │ │ scheduler │ │ generator │ │ sync │ │
│ └────┬────┘ └─────┬─────┘ └─────┬─────┘ └──┬───┘ │
├───────┴─────────────┴──────────────┴───────────┴──────────────┤
│ DOMAIN LAYER │
│ ┌───────┐ ┌───────┐ ┌──────┐ ┌─────┐ ┌───────┐ ┌─────┐ │
│ │ users │ │ oauth │ │ files│ │ ai │ │ agent │ │ mcp │ ... │
│ └───┬───┘ └───┬───┘ └──┬───┘ └──┬──┘ └───┬───┘ └──┬──┘ │
├──────┴─────────┴────────┴────────┴────────┴────────┴──────────┤
│ INFRASTRUCTURE LAYER │
│ ┌──────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │ database │ │ logging │ │ config │ │ events │ │ security │ │
│ └────┬─────┘ └────┬────┘ └────┬───┘ └────┬───┘ └────┬─────┘ │
├───────┴────────────┴───────────┴──────────┴──────────┴────────┤
│ SHARED LAYER │
│ ┌────────┐ ┌────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ models │ │ traits │ │ identifiers │ │ extension │ ... │
│ └────────┘ └────────┘ └─────────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────┘
Shared Layer
Purpose: Pure types with zero dependencies on other SystemPrompt crates.
Characteristics
- No database access
- No side effects
- No external I/O
- Serializable types
- Compile-time only
Crates
| Crate | Description |
|---|---|
systemprompt-models |
Domain models, DTOs, validation types |
systemprompt-traits |
Core trait definitions, re-exports provider-contracts |
systemprompt-identifiers |
Typed IDs with compile-time safety |
systemprompt-extension |
Extension framework, inventory registration |
systemprompt-provider-contracts |
LlmProvider, ToolProvider, Job traits |
systemprompt-client |
Generic HTTP client abstraction |
systemprompt-template-provider |
Template loading traits |
Example: Typed Identifiers
use systemprompt_identifiers::{UserId, TenantId};
// Type-safe - can't accidentally use UserId where TenantId expected
fn get_user(tenant: TenantId, user: UserId) -> Result<User> {
// ...
}
Infrastructure Layer
Purpose: Stateless utilities providing foundational services.
Characteristics
- May have external I/O
- No business logic
- Reusable across domains
- Configuration-driven
Crates
| Crate | Description |
|---|---|
systemprompt-database |
SQLx abstraction, connection pooling, migrations |
systemprompt-logging |
Tracing setup, structured logging, log sinks |
systemprompt-config |
Configuration loading, profile management |
systemprompt-events |
Event bus, broadcasters, SSE infrastructure |
systemprompt-security |
JWT validation, token extraction, cookies |
systemprompt-cloud |
Cloud API client, tenant management |
systemprompt-loader |
File discovery, module loading |
Example: Database Layer
use systemprompt_database::DatabasePool;
// Get a connection from the pool
let pool = DatabasePool::connect(&config).await?;
// Use SQLx for queries
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_one(&pool)
.await?;
Domain Layer
Purpose: Full bounded contexts with SQL, repositories, and services.
Characteristics
- Contains business logic
- Owns its database tables
- Has repository pattern
- Emits and handles events
- Clear public API
Required Structure
Each domain crate follows this structure:
domain/{name}/
├── Cargo.toml
├── schema/ # SQL files
│ └── {table}.sql
└── src/
├── lib.rs # Public API
├── extension.rs # Extension registration
├── models/ # Domain types
├── repository/ # Data access
└── services/ # Business logic
Crates
| Crate | Description |
|---|---|
systemprompt-users |
User management, roles, permissions |
systemprompt-oauth |
OAuth2/OIDC flows, token management |
systemprompt-files |
File storage, metadata, serving |
systemprompt-analytics |
Metrics collection, reporting |
systemprompt-content |
Markdown processing, publishing |
systemprompt-mcp |
MCP server registry, tool execution |
systemprompt-ai |
LLM integration, request logging |
systemprompt-agent |
A2A protocol, agent registry |
systemprompt-templates |
Template registry, rendering |
Example: Domain Service
pub struct ContentService {
repository: Arc<dyn ContentRepository>,
search: Arc<SearchService>,
events: Arc<EventBus>,
}
impl ContentService {
pub async fn publish(&self, id: ContentId) -> Result<Content> {
let content = self.repository.find_by_id(id).await?
.ok_or(ContentNotFound)?;
// Business logic
let published = content.publish()?;
// Persist
self.repository.save(&published).await?;
// Index for search
self.search.index(&published).await?;
// Notify other domains
self.events.emit(ContentPublished { id }).await;
Ok(published)
}
}
Application Layer
Purpose: Orchestration layer without business logic.
Characteristics
- Coordinates domain services
- Manages lifecycle
- No direct database access
- No business rules
Crates
| Crate | Description |
|---|---|
systemprompt-runtime |
AppContext, module registry, lifecycle |
systemprompt-scheduler |
Cron scheduling, job execution |
systemprompt-generator |
Static site generation |
systemprompt-sync |
Cloud synchronization, content sync |
Example: Runtime
pub struct AppContext {
pub config: Config,
pub database: DatabasePool,
pub events: EventBus,
pub extensions: ExtensionRegistry,
}
impl AppContext {
pub async fn new(config: Config) -> Result<Self> {
// Initialize all layers
let database = DatabasePool::connect(&config.database).await?;
let events = EventBus::new();
let extensions = ExtensionRegistry::discover();
// Run extension migrations
for ext in extensions.schema_extensions() {
ext.migrate(&database).await?;
}
Ok(Self { config, database, events, extensions })
}
}
Entry Layer
Purpose: Binary entry points that wire everything together.
Characteristics
- Creates AppContext
- Configures routes/commands
- Handles signals
- No business logic
Crates
| Crate | Description |
|---|---|
systemprompt-api |
Axum HTTP server, route configuration |
systemprompt-cli |
CLI application, command handlers |
Example: API Entry
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::load()?;
let ctx = AppContext::new(config).await?;
let app = Router::new()
.merge(ctx.extensions.routes())
.layer(middleware_stack(&ctx));
axum::serve(listener, app).await?;
Ok(())
}
Dependency Rules
- Same layer: Crates can depend on other crates in the same layer
- Lower layers: Crates can depend on any crate in lower layers
- Never upward: A crate cannot depend on crates in higher layers
Valid Dependencies
api → runtime → users → database → models ✓
cli → scheduler → content → events → traits ✓
Invalid Dependencies
models → database ✗ (shared cannot depend on infra)
database → users ✗ (infra cannot depend on domain)
users → runtime ✗ (domain cannot depend on app)
Next Steps
- Extension System - Building extensions
- Crate Reference - Detailed crate documentation