Layer System

Deep dive into SystemPrompt's five-layer architecture

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

  1. Same layer: Crates can depend on other crates in the same layer
  2. Lower layers: Crates can depend on any crate in lower layers
  3. 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