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
Frontend runtime — `@elyra/runtime`

Frontend runtime — @elyra/runtime

The npm package the Svelte app imports. It speaks the binary wire format so you don't have to.

import { invoke, channel, CommandError } from "@elyra/runtime";

invoke(command, ...args)

Call a Rust #[command] by name. Arguments are MessagePack-encoded; the result is decoded into the resolved type.

const greeting = await invoke<string>("greet", "world");
const sum = await invoke<number>("add", 2, 3);

If the command returns an error (a Result::Err, or a middleware/decode failure), the promise rejects with a CommandError carrying the command name and message.

try {
  await invoke("checked_div", 1, 0);
} catch (e) {
  if (e instanceof CommandError) console.error(e.message);
}

channel(name)

Subscribe to a server-pushed event channel. The return value is a Svelte-readable store, so $channel(...) works in a component; it's also usable standalone.

<script>
  import { channel } from "@elyra/runtime";
  const cursor = channel("cursor");
</script>
<pre>{JSON.stringify($cursor)}</pre>
const unsub = channel<number>("tick").subscribe((v) => { /* ... */ });
// later: unsub();

All channels are multiplexed over one long-poll connection with automatic reconnect/backoff.

The generated api.* facade

After rata codegen you get bindings.ts with a fully typed facade — prefer it over stringly-typed invoke:

import { api } from "./bindings";
const todos = await api.list_todos();        // Promise<Todo[]>
const todo  = await api.add_todo("milk");    // Promise<Todo>

The facade delegates to invoke under the hood, so error handling is identical.

Live queries — live.*

A #[command(live)] (see commands) gets a store in the generated bindings next to its api.* call. Rust re-runs the command whenever the data it read changes — written in this window, another window, or a background job — and the store updates:

<script>
  import { live } from "./bindings";
  let query = $state({ search: null, sort: "name", direction: "asc", page: 1, per_page: 25 });
  const customers = $derived(live.customers_index(query));
</script>

{#if $customers.loading}
  <p>Loading…</p>
{:else if $customers.error && !$customers.value}
  <p>{$customers.error.message}</p>
{:else}
  {#each $customers.value?.data ?? [] as c (c.id)}<p>{c.name}</p>{/each}
{/if}

The store's value is a Live<T>:

Field
value the latest result (undefined until the first one)
error the latest re-run's failure — a CommandError, or a ValidationError — with the previous value kept
loading true until the first result

The subscription opens with the store's first listener and closes with its last, so a component's $store is the whole lifecycle. With $derived, a changed query opens a new subscription and closes the old one. Without codegen, live<T>(command, ...args) from @elyra/runtime does the same, untyped.

Origin, CORS, and the IPC token

Everything is same-origin under elyra://localhost (the app is served there, IPC and events too), so fetch needs no CORS handling in production — and the shell sends no Access-Control-Allow-* headers there, so a foreign origin can't read an IPC response. Under rata dev the page loads from Vite's http://localhost:5173; only then does the shell add CORS, and only for that exact origin (from ELYRA_DEV_URL).

On top of the origin rule, every /__* request must carry this run's IPC token. The shell generates a random token per launch and injects it into the webview before any page script runs (globalThis.__ELYRA__.token); @elyra/runtime attaches it as x-elyra-token automatically. Requests without it get 403 and the runtime throws ForbiddenError:

import { invoke, ForbiddenError } from "@elyra/runtime";

try {
  await invoke("greet", "World");
} catch (e) {
  if (e instanceof ForbiddenError) {
    // Refused at the security gate. `e.detail` is the shell's own explanation:
    // a bad token, an ungranted capability, or a command's `can = "…"` ability.
    console.error(e.detail);
  }
}

ForbiddenError covers every refusal at the gate, not only the token — an ungranted capability and an ungranted command ability arrive the same way. e.detail names which one, so read it before assuming the token is at fault.

If you talk to the bridge without @elyra/runtime (a raw fetch), send both x-elyra-token and x-elyra-client-id yourself.

Routing

A hash-based router (#/customers/12) — it needs nothing from the shell, and reload / Back / Forward behave as in a browser. rata new scaffolds it: a route table in src/routes.js, pages in src/pages/, and a Router.svelte that renders the current one.

// src/routes.js
export const routes = {
  "/": () => import("./pages/Home.svelte"),
  "/customers": () => import("./pages/Customers.svelte"),
  "/customers/new": () => import("./pages/CustomerForm.svelte"),
  "/customers/:id": () => import("./pages/Customer.svelte"),
  "/*": () => import("./pages/NotFound.svelte"),
};

Each page is loaded on demand (its own chunk) and receives params and query:

<!-- pages/Customer.svelte, for /customers/12?tab=notes -->
<script>
  let { params, query } = $props();   // { id: "12" }, { tab: "notes" }
</script>
  • Precedence: the most specific pattern wins, whatever the table's order — static segments beat :params, and a * wildcard only matches when nothing exact does. So /customers/new beats /customers/:id.
  • Links: <a href={href("/customers/12", { tab: "notes" })}> builds the #… href; $route.path / $route.query give the current location (e.g. to mark the active nav item).
  • In code: navigate("/customers"); navigate("/login", { replace: true }) for redirects, so Back skips them.
  • Deep links: deepLinkPath("myapp://customers/12") → /customers/12, so a URL from the elyra:deep-link channel can be handed to navigate.
  • matchRoute and resolveRoute are exported for other renderers — the router itself has no Svelte dependency.

Related