Skip to content
Lenso
English
Esc
navigateopen⌘Jpreview
On this page

Platform Concepts

The Lenso primitives that make modules explicit, observable, and safe to split later.

Lenso is not just a starter template. The useful part is the set of small, host-owned primitives that make a business system understandable to humans, agents, and operators.

The core idea

A Lenso app starts as one deployable Rust backend:

  • the API serves product routes and Runtime Console
  • the worker owns runtime queues, outbox dispatch, and lifecycle work
  • the migration app owns database setup
  • modules declare what they contribute
  • the host controls loading, auth, config, observability, and release shape

That gives a team the ergonomics of a modular monolith without hiding the seams needed to split a module into a service later.

Manifest data, behavior seams

Every module exposes pure metadata through ModuleManifest. Executable behavior stays behind a Linked Rust binding or an independently running Service’s provider contract.

use lenso::{
    ModuleManifest, ModuleHttpMethod, ModuleHttpRoute, RuntimeFunctionDeclaration,
    RuntimeSurface,
};

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support")
        .capabilities(vec![
            "support.tickets.read".to_owned(),
            "support.tickets.write".to_owned(),
        ])
        .http_routes(vec![ModuleHttpRoute {
            method: ModuleHttpMethod::Post,
            path: "/v1/support/tickets".to_owned(),
            capability: Some("support.tickets.write".to_owned()),
            display_name: Some("Create Ticket".to_owned()),
            story_title: Some("Ticket Created".to_owned()),
        }])
        .runtime(RuntimeSurface {
            functions: vec![RuntimeFunctionDeclaration {
                name: "support.ticket.escalate.v1".to_owned(),
                version: 1,
                queue: "support".to_owned(),
                input_schema: Some("support.ticket.escalate.v1".to_owned()),
                retry_policy: None,
            }],
            schedules: vec![],
        })
        .build()
}

The manifest is what Lenso Console, install checks, and agent-authored smoke checks can reason about. It is not where SQL handlers, runtime executors, or Service clients live.

Host-owned lifecycle

Lifecycle is also declaration data. A module can say which startup checks and activation jobs it needs, but the worker decides when to run them and records the result through runtime-owned paths.

use lenso::{
    LifecycleActivationJobDeclaration, LifecycleActivationRunPolicy,
    LifecycleStartupCheckDeclaration, LifecycleStartupCheckKind, LifecycleSurface,
    ModuleManifest, RuntimeFunctionDeclaration, RuntimeSurface,
};

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support")
        .runtime(RuntimeSurface {
            functions: vec![RuntimeFunctionDeclaration {
                name: "support.ticket.reindex.v1".to_owned(),
                version: 1,
                queue: "support".to_owned(),
                input_schema: Some("support.ticket.reindex.v1".to_owned()),
                retry_policy: None,
            }],
            schedules: vec![],
        })
        .lifecycle(LifecycleSurface {
            startup_checks: vec![LifecycleStartupCheckDeclaration {
                name: "reindex function is registered".to_owned(),
                required: true,
                check: LifecycleStartupCheckKind::FunctionRegistered {
                    function_name: "support.ticket.reindex.v1".to_owned(),
                },
            }],
            activation_jobs: vec![LifecycleActivationJobDeclaration {
                name: "warm ticket index".to_owned(),
                function_name: "support.ticket.reindex.v1".to_owned(),
                run_policy: LifecycleActivationRunPolicy::EveryStartup,
                input: serde_json::json!({ "reason": "startup" }),
                required: false,
            }],
        })
        .build()
}

There is no generic module startup callback. That is deliberate: startup work is visible, retryable, and comparable across Linked Modules and Services.

Runtime evidence

Runtime Console is not a decorative dashboard. It is the proof that the host saw the work.

Lenso’s Runtime Stories turn request ids, correlation ids, outbox rows, function runs, provider calls, and telemetry spans into an operator-readable business narrative.

Source What it proves
platform.outbox module events were committed and dispatched
runtime.function_runs runtime work was enqueued, retried, completed, or failed
platform.story_events business and operator events were projected into stories
provider invocation evidence Service traffic crossed an owned boundary

When a module is done, the answer should not be “it compiles.” It should be “the host loaded it, the action appears, the runtime work is visible, and the story has evidence.”

Service boundaries remain owned

Services are for real boundaries: a separate runtime, separate owner, separate deploy cadence, Store ownership, or a trust boundary. They do not become a second kind of Module.

The host still owns:

  • Module composition and manifest lint results
  • capability enforcement at Host entrypoints
  • caller auth and capability checks
  • request and response limits
  • header allowlists
  • runtime queues and retry policy
  • outbox dispatch
  • Runtime Console evidence

The Host may authenticate to a Service provider, but it does not forward the caller’s bearer token or cookies.

Systems connect services and modules

When a product grows beyond one provider, lenso.system.json names the whole System graph. Services own processes and deployment targets. Modules own business capabilities. The system plane connects them with environments and capability dependencies so CLI checks and Runtime Console can show whether the system is coherent.

See Service System Plane for the lenso.system.v1 contract and CLI flow.

The split rule

Start Linked when the Module belongs to the app. Extract a Service when one boundary is real enough to pay for:

  • different runtime
  • different deployment cadence
  • different team ownership
  • different trust model
  • independent operational scaling

Until then, the lazy path is the good path: one host, explicit module seams, and visible evidence.

Last updated on August 1, 2026

Was this page helpful?