Template Architecture

Understand how the SystemPrompt template wraps systemprompt-core to give you a fully functional Rust binary that you own and control.

When you clone the SystemPrompt template, you receive a complete Rust project that compiles into a binary you fully own. This is not a hosted service or a managed platform. The binary runs on your infrastructure, under your control, with your extensions compiled directly into it.

The template is a thin wrapper around systemprompt-core, the framework library. Core provides the runtime, CLI, database abstractions, and extension discovery. Your template provides the extensions that add your specific functionality: web routes, database schemas, background jobs, MCP tools, and custom CLI commands.

What is the Template?

The template is your project. When you run cargo build, you produce a single binary named systemprompt that contains everything: the core runtime, your extensions, and all their dependencies. There is no separate service to deploy, no external process to manage. Your binary is self-contained.

This ownership model has important implications. You control the release cycle. You choose when to upgrade core. You can fork and modify any extension. You can add proprietary functionality that never leaves your infrastructure. The binary is yours to deploy, distribute, or even sell.

The relationship between your template and core is a standard Rust dependency relationship. Core is a library crate published to crates.io. Your template depends on it like any other crate. During development, you can use a local checkout of core via Cargo's path overrides. In production, you depend on a published version.

Entry Point

The template's entry point is remarkably simple. The entire main.rs file is fewer than ten lines:

use systemprompt_template as _;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    systemprompt_template::__force_extension_link();
    systemprompt_template::cli::run().await
}

This minimal entry point does three things. First, it imports the template library to bring extensions into scope. Second, it calls __force_extension_link() to ensure extensions survive linker optimization. Third, it delegates to core's CLI runner, which handles everything else: argument parsing, configuration loading, database connections, extension discovery, and command execution.

You can customize this entry point. Add pre-initialization logic, custom panic handlers, or environment setup. The core runtime is a library function you call, not a framework that calls you.

Extension Linking

Extensions register themselves at compile time using the inventory crate. This crate uses linker sections to collect items across crate boundaries without runtime overhead. When core starts, it iterates over all registered extensions and loads them.

The challenge is that Rust's linker is aggressive about removing unused code. If an extension crate has no direct function calls from main, the linker may strip it entirely, including its inventory registration. The __force_extension_link() function solves this by explicitly referencing a constant from each extension:

pub fn __force_extension_link() {
    let _ = core::hint::black_box(&web::WebExtension::PREFIX);
}

The black_box hint tells the compiler to treat this value as externally observable, preventing optimization. This single line ensures the web extension crate stays linked, and with it, all extensions registered via register_extension!().

When you add a new library extension, you must add a similar reference to this function. Otherwise, your extension may silently vanish from the final binary.

Adding a New Extension

To add a new library extension to your template:

1. Create the extension crate:

cargo new extensions/web/my-feature --lib

2. Add workspace membership in the root Cargo.toml:

[workspace]
members = [
    "extensions/web",
    "extensions/web/my-feature",  # Add this
]

3. Implement the Extension trait in your new crate. Define metadata, schemas, routes, and jobs as needed. Register with the register_extension!() macro.

4. Reference the extension in lib.rs:

pub use my_feature_extension as my_feature;

pub fn __force_extension_link() {
    let _ = core::hint::black_box(&web::WebExtension::PREFIX);
    let _ = core::hint::black_box(&my_feature::MyExtension::PREFIX);  # Add this
}

5. Rebuild:

cargo build

The extension is now part of your binary. Its schemas run during migrations, its routes mount on startup, and its jobs register with the scheduler.

For complete implementation details including the Extension trait, database schemas, and HTTP routes, see Library Extensions.

What the Runtime Does

When cli::run() executes, core performs a startup sequence that brings your extensions to life:

Extension Discovery: Core iterates over inventory::iter::<Extension>() to find all registered extensions. It sorts them by priority to ensure correct initialization order.

Configuration Loading: Each extension declares a config_prefix(). Core loads environment variables with that prefix and passes validated configuration to the extension.

Database Migrations: Extensions return SchemaDefinition values from their schemas() method. Core executes these in migration_weight() order, creating tables and indexes your extension needs.

Router Mounting: Extensions that implement router() receive an ExtensionContext with database access. They return an Axum router that core mounts at the extension's base path.

Job Registration: Background jobs from jobs() are registered with the scheduler. They execute according to their cron schedules or on-demand via CLI.

Provider Registration: Page data providers, component renderers, and prerenderers register with the template engine. They inject data and render partials when pages are served.

This entire sequence is deterministic. The same binary with the same configuration produces the same behavior. This predictability is essential for agent execution, where reproducibility matters.