Architecture
Elyra is a Laravel-shaped desktop framework: the same ergonomic building blocks — an application container, service providers, middleware, a typed client — but compiled to a single native binary with no runtime interpreter.
The Laravel map
| Laravel | Elyra |
|---|---|
| Application + container | App + Container (ctx.get::<T>()) |
| ServiceProvider | Provider (register / boot) |
| routes/web.php | commands![...] |
| Controller action | #[command] async fn |
| Middleware | pipeline in CommandRegistry::dispatch |
| Broadcasting + Echo | EventBus + channel() store |
| Facades / HTTP client | generated api.* |
| Eloquent | #[derive(Model)] |
| Artisan | Ratatosk (rata) |
| Blade / Livewire | Svelte 5 (runes) |
Processes and threads
- The event loop + webview live on the main thread (required on macOS).
- A separate multi-thread tokio runtime owns all IPC work.
- The custom-protocol handler is asynchronous: each request is spawned onto tokio and responded to from there, so the UI thread never blocks on a command or an event long-poll.
┌─────────────── main thread ───────────────┐ ┌──── tokio runtime ────┐
│ tao event loop ── wry webview (WebKit) │ │ command dispatch │
│ └ elyra:// custom protocol handler ──────┼────▶│ middleware pipeline │
│ │◀────┼─ EventBus flushes │
└────────────────────────────────────────────┘ └───────────────────────┘
Request lifecycle (a command)
- Frontend calls
invoke("name", ...args)(orapi.name(...)). @elyra/runtimeMessagePack-encodes the args andfetcheselyra://localhost/__cmd/name.- The protocol handler spawns the request on tokio.
CommandRegistry::dispatchruns the middleware pipeline, then the command.#[command]decodes the arg tuple, runs yourasync fn, and MessagePack-encodes the result (or maps aResult::Errto an error response).- The bytes are returned; the runtime decodes them into the resolved value.
See wire format for the exact bytes.
The IPC boundary
The webview is untrusted: anything executing in the page — your code, a dependency,
injected content — can reach /__*. Three mechanisms gate it, all enforced in
shell::route before dispatch:
- a random per-run token injected into the webview before any page script runs,
- origin isolation — no CORS headers at all in a production build,
- a capability model where destructive routes are opt-in,
plus structural limits on request bodies (size + nesting depth). See security.
State ownership
Rust owns the state; the frontend is a projection. Instead of one IPC round per
change, the EventBus accumulates events and flushes them as a
single batch to a long-poll the frontend holds open — binary, no base64.
Events: one queue per window
EventBus keeps a queue per connected webview, keyed on a client id the runtime
sends. An emit fans out to every window; a single shared queue meant whichever
window polled first consumed the batch and the others silently lost it. Events
emitted before any window connects are held for the first poll.
Crates
framework/ elyra App, Container/Ctx, Command, EventBus, shell (tao+wry),
security policy, windows, tray, updater, codegen,
Log/Config/Secrets/testing
macros/ elyra-macros #[command], #[derive(Model)]
database/ elyra-db Database (sqlx Any), schema builder, migrations, models
— no GUI deps, so the CLI can use it without tao/wry
ai/ elyra-ai the AI SDK (agents, tools, embeddings, …)
substrate/ substrate-core the shared Cache/Storage/Queue contracts
ratatosk/ ratatosk the `rata` CLI
runtime/ @elyra/runtime invoke(), channel(), the generated api.*
elyra-db is deliberately GUI-free so rata migrate (and any headless tool) can
drive the database without pulling in the windowing stack.
Performance principles
- Binary IPC — MessagePack over the custom protocol via
fetch(). No JSON in the hot path; streaming andArrayBufferfor free. - Rust owns state — diffs pushed via the
EventBus, batched per flush. - Assets from memory —
rust-embed+ protocol handler, no disk I/O at start. - Never block the UI thread — every command runs on tokio.