Skip to main content

Extension Initialization

Extensions wire into AppContext after discovery: schema install, migration run, router mount, and job registration run in dependency order before serving.

After discovery, extensions integrate with AppContext during runtime startup.

Startup Sequence

1. ProfileBootstrap::init()
   |
2. SecretsBootstrap::init()
   |
3. CredentialsBootstrap::init()
   |
4. Config::init()
   |
5. ExtensionRegistry::discover()
   |
6. ExtensionRegistry::validate()
   |
7. AppContext::builder().build()
   |
8. Schema Installation
   |
9. Migration Execution
   |
10. Router Mounting
   |
11. Job Registration
   |
12. Server Start

AppContext Integration

AppContext groups its state into internal planes (data, config, plugins, subsystems) and is constructed through a builder:

pub struct AppContext {
    // Internal planes: DataPlane, ConfigPlane, Plugins, Subsystems
    // Access via methods like config(), database(), extension_registry()
}

impl AppContext {
    pub async fn new() -> RuntimeResult<Self> {
        Self::builder().build().await
    }

    pub fn builder() -> AppContextBuilder {
        AppContextBuilder::new()
    }
}

The builder controls extension wiring. By default extensions are discovered via inventory and schema installation is off; the serve path turns it on:

let ctx = AppContext::builder()
    .with_extensions(registry)   // optional; defaults to ExtensionRegistry::discover()
    .with_migrations(true)       // install schemas + run migrations during build()
    .build()
    .await?;

During build(), the runtime discovers the registry when one was not supplied, calls registry.validate(), and (when migrations are enabled) installs every extension's schemas and applies pending migrations before the context is returned.

ExtensionContext

Extensions access runtime services via ExtensionContext:

pub trait ExtensionContext: Send + Sync {
    fn config(&self) -> Arc<dyn ConfigProvider>;
    fn database(&self) -> Arc<dyn DatabaseHandle>;
    fn get_extension(&self, id: &str) -> Option<Arc<dyn Extension>>;

    fn has_extension(&self, id: &str) -> bool {
        self.get_extension(id).is_some()
    }
}

pub type DynExtensionContext = Arc<dyn ExtensionContext>;

The runtime hands an ExtensionContext implementation to each extension during router resolution, so extensions can read configuration, reach the database handle, and look up sibling extensions by id.

Schema Installation

Schemas execute in the registry's dependency-sorted extension order. Each SchemaDefinition carries its SQL inline:

pub struct SchemaDefinition {
    pub table: String,
    pub sql: String,
    pub required_columns: Vec<String>,
    pub schema: Option<String>,
}

For each definition the installer executes the SQL, then verifies that every entry in required_columns exists on the declared table before startup continues.

Router Mounting

After context creation, routers mount to the server:

async fn build_router(ctx: Arc<AppContext>) -> Router {
    let mut router = Router::new();

    for ext in ctx.extension_registry.iter() {
        if let Some(ext_router) = ext.router(&*ctx) {
            router = router.nest(ext_router.base_path, ext_router.router);
        }
    }

    router.with_state(ctx)
}

Job Registration

Jobs register with the scheduler:

async fn register_jobs(ctx: &AppContext, scheduler: &Scheduler) {
    for ext in ctx.extension_registry.iter() {
        for job in ext.jobs() {
            scheduler.register(job.clone()).await;
        }
    }
}

Provider Collection

Providers are collected for the generator:

fn collect_page_providers(registry: &ExtensionRegistry) -> Vec<Arc<dyn PageDataProvider>> {
    registry.iter()
        .flat_map(|ext| ext.page_data_providers())
        .collect()
}

fn collect_component_renderers(registry: &ExtensionRegistry) -> Vec<Arc<dyn ComponentRenderer>> {
    registry.iter()
        .flat_map(|ext| ext.component_renderers())
        .collect()
}

Error Handling

Initialization errors:

pub enum LoaderError {
    MissingDependency { extension: String, dependency: String },
    DuplicateExtension(String),
    InitializationFailed { extension: String, message: String },
    SchemaInstallationFailed { extension: String, message: String },
    MigrationFailed { extension: String, message: String },
    MigrationNotReversible { extension: String, version: u32 },
    ConfigValidationFailed { extension: String, message: String },
    ReservedPathCollision { extension: String, path: String },
    InvalidBasePath { extension: String, path: String },
    DependencyCycle { chain: String },
    CrossExtensionAlterUndeclared { extension: String, table: String },
    DuplicateTableOwner { table: String, extension_a: String, extension_b: String },
}

Graceful Shutdown

On shutdown:

  1. Stop accepting new requests
  2. Wait for in-flight requests
  3. Stop scheduler
  4. Close database connections
  5. Log final state

Debugging

Startup Logs

INFO Starting systemprompt.io
INFO Loading profile: local
INFO Discovering extensions...
INFO Found 12 extensions
INFO Validating dependencies...
INFO Installing schemas...
INFO Running migrations...
INFO Mounting routers...
INFO Registering jobs...
INFO Server listening on 0.0.0.0:8080

Extension Status

systemprompt plugins list
systemprompt plugins validate

Database Status

systemprompt infra db status