Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Container & providers

Container & providers

The container

Container is a type-keyed registry of shared singletons (Laravel's container). You rarely touch it directly — bind via App::bind or a provider, resolve via Ctx.

App::new()
    .bind(Config { greeting: "hi".into() })   // bind a singleton
    .commands(commands![greet]);

Bindings must be Send + Sync + 'static (commands run on tokio). A later bind of the same type replaces the previous one.

Binding a trait (interface → implementation)

Laravel's bind(Interface::class, Impl::class). Bind under the trait object and depend on the trait, not the backend:

trait Mailer: Send + Sync { fn send(&self, to: &str) -> Result<()>; }

App::new().bind_as::<dyn Mailer>(Arc::new(SmtpMailer::new()));

#[command]
async fn invite(ctx: Ctx, email: String) -> Result<()> {
    ctx.get::<dyn Mailer>().send(&email)        // Arc<dyn Mailer>
}

Swap the implementation in one place — a LogMailer in development, a fake in a test — without touching a command. This is also how to bind the substrate contracts: bind_as::<dyn substrate::Cache>(..).

Lazy singletons

bind_lazy builds a singleton the first time it's resolved, with a full Ctx:

c.bind_lazy::<dyn Mailer>(|ctx| {
    Arc::new(SmtpMailer::new(&ctx.get::<MailConfig>().host))
});

This is how a provider's register can depend on another service: register itself must not resolve anything (other providers may not have bound it yet), but a lazy binding runs after every provider has registered, so order stops mattering. It's still a singleton — built once, then shared.

A dependency cycle between lazy bindings (A needs B needs A) panics with the chain — circular dependency between lazy bindings: app::A -> app::B -> app::A — instead of deadlocking.

Ctx

The context handed to every command. Cheap to clone (an Arc bump).

#[command]
async fn greet(ctx: Ctx, name: String) -> String {
    let cfg = ctx.get::<Config>();        // Arc<Config>; panics if unbound
    let db  = ctx.try_get::<Database>();  // Option<Arc<Database>>
    format!("{} {name}", cfg.greeting)
}
  • get::<T>() -> Arc<T> — panics with a clear message if T isn't bound (a missing binding is a wiring bug, so fail loudly).
  • try_get::<T>() -> Option<Arc<T>> — fallible resolution.
  • has::<T>() -> bool — whether anything is bound, without building it.
  • T may be a trait object in all three: ctx.get::<dyn Mailer>().
  • dispatch(event).await — send a domain event to its listeners.

The EventBus and the Dispatcher are always bound, and — with the relevant features — so are Database (via App::database) and Windows.

Providers

A Provider is the idiomatic place to wire up a slice of the app (Laravel's ServiceProvider). Two phases:

  • register(&mut Container) — bind services. Runs for all providers first; don't resolve other services here, they may not be bound yet.
  • boot(&Ctx) — runs after everything is registered, with a populated context. Resolve dependencies, seed state, spawn setup.
use elyra::{Container, Ctx, Provider};

struct ConfigProvider;

impl Provider for ConfigProvider {
    fn register(&self, c: &mut Container) {
        c.bind(Config { greeting: "hi".into() });
    }
    fn boot(&self, ctx: &Ctx) {
        // container is fully populated here
        let _ = ctx.get::<Config>();
    }
}

App::new().provider(ConfigProvider).run();

Both methods have default no-op bodies, so implement only the phase you need.

Lifecycle

App::run (and the testable App::prepare) assemble the app in this order:

  1. App::bind(..) values are placed in the container.
  2. Each provider's register runs.
  3. The EventBus (and Database/Windows if enabled) are bound.
  4. The Ctx is built.
  5. Each provider's boot runs.
  6. The window(s) open and the event loop runs.

Related