There are two distinct dimensions: embedding the frontend into your app, and embedding blit server into your own service.
@blit-sh/react and @blit-sh/solid are workspace-first. Both are thin wrappers over @blit-sh/core's BlitTerminalSurface. A BlitWorkspace owns connections, each connection owns terminals, and each BlitTerminal renders a terminal by ID.
import {
BlitTerminal,
BlitWorkspaceProvider,
useBlitFocusedSession,
useBlitSessions,
useBlitWorkspace,
} from "@blit-sh/react";
import { BlitWorkspace, WebSocketTransport } from "@blit-sh/core";
import { useEffect, useMemo } from "react";
function EmbeddedBlit({ wasm, passphrase }: { wasm: any; passphrase: string }) {
const transport = useMemo(
() => new WebSocketTransport("wss://example.com/blit", passphrase),
[passphrase],
);
const workspace = useMemo(
() =>
new BlitWorkspace({
wasm,
connections: [{ id: "default", transport }],
}),
[transport, wasm],
);
useEffect(() => () => workspace.dispose(), [workspace]);
return (
<BlitWorkspaceProvider workspace={workspace}>
<TerminalScreen />
</BlitWorkspaceProvider>
);
}
function TerminalScreen() {
const workspace = useBlitWorkspace();
const sessions = useBlitSessions();
const focusedSession = useBlitFocusedSession();
useEffect(() => {
if (sessions.length > 0) return;
void workspace.createSession({
connectionId: "default",
rows: 24,
cols: 80,
});
}, [sessions.length, workspace]);
return (
<BlitTerminal
sessionId={focusedSession?.id ?? null}
style={{ width: "100%", height: "100vh" }}
/>
);
}Read-only terminals use the same sizing behavior as writable terminals; readOnly only disables mutating input. Terminals resize the remote session to their available grid by default. Pass resizable={false} for passive previews or read-only transports that must preserve the host dimensions; the canvas is then contained and centered within the embedding element.
| API | Purpose |
|---|---|
new BlitWorkspace({ wasm, connections }) |
Create a workspace with one or more transports |
BlitWorkspaceProvider |
Put the workspace, palette, and font settings in context |
useBlitWorkspace() |
Get the imperative workspace object |
useBlitWorkspaceState() |
Read the full reactive workspace snapshot |
useBlitConnection(connectionId?) |
Read one connection snapshot |
useBlitSessions() |
Read all terminals |
useBlitFocusedSession() |
Read the currently focused terminal |
useBlitWorkspaceConnection(workspace, id, transport) |
Manage a connection lifecycle with cleanup |
BlitTerminal |
Render one terminal by sessionId |
import {
BlitTerminal,
BlitWorkspaceProvider,
createBlitWorkspace,
createBlitWorkspaceState,
createBlitSessions,
useBlitFocusedSession,
} from "@blit-sh/solid";
import { BlitWorkspace } from "@blit-sh/core";
import { createSignal, onCleanup, createEffect } from "solid-js";
function EmbeddedBlit(props: { wasm: any; passphrase: string }) {
const workspace = new BlitWorkspace({
wasm: props.wasm,
connections: [
{
id: "default",
transport: {
type: "websocket",
url: "wss://example.com/blit",
passphrase: props.passphrase,
},
},
],
});
onCleanup(() => workspace.dispose());
return (
<BlitWorkspaceProvider workspace={workspace}>
<TerminalScreen />
</BlitWorkspaceProvider>
);
}
function TerminalScreen() {
const workspace = createBlitWorkspace();
const sessions = createBlitSessions();
const focusedSession = () => useBlitFocusedSession(workspace);
createEffect(() => {
if (sessions().length > 0) return;
workspace.createSession({ connectionId: "default", rows: 24, cols: 80 });
});
return (
<BlitTerminal
sessionId={focusedSession()?.id ?? null}
style={{ width: "100%", height: "100vh" }}
/>
);
}| API | Purpose |
|---|---|
new BlitWorkspace({ wasm, connections }) |
Create a workspace with one or more transports |
BlitWorkspaceProvider |
Put the workspace, palette, and font settings in context |
createBlitWorkspace() |
Get the imperative workspace object from context |
createBlitWorkspaceState(workspace?) |
Reactive signal tracking the workspace snapshot |
createBlitSessions(workspace?) |
Reactive signal tracking all terminals |
useBlitSession(workspace, sessionId) |
Look up a single terminal by ID (non-reactive) |
useBlitFocusedSession(workspace) |
Look up the focused terminal (non-reactive) |
useBlitConnection(workspace, sessionId) |
Look up a connection snapshot (non-reactive) |
createBlitWorkspaceConnection(workspace, id, transport) |
Manage a connection lifecycle with onCleanup |
BlitTerminal |
Render one terminal by sessionId |
BlitSurfaceView renders a single Wayland surface from a terminal's compositor. The server encodes each surface as H.264 or AV1; the component decodes via WebCodecs and draws to a canvas.
By default the view owns its surface's size: it resizes the surface to fill its
container, and is fully interactive. Pass resizable={false} for a passive
preview — a dock card, a switcher thumbnail — that shares another view's stream.
Such a view is served a fixed downscale capped at a thumbnail cadence and takes
no input at all, so it is the wrong choice for anything the user clicks in.
zoom scales the surface independently of the pane's pixel size: zoomMode
"relative" (the default) multiplies the display's DPI by zoom, while
"exact" uses zoom as the absolute surface scale. Only resizable views drive
the scale.
import { BlitSurfaceView } from "@blit-sh/react";
function AppWindow({
connectionId,
surfaceId,
}: {
connectionId: string;
surfaceId: number;
}) {
return (
<BlitSurfaceView
connectionId={connectionId}
surfaceId={surfaceId}
style={{ width: 800, height: 600 }}
/>
);
}touchMode chooses how touchscreen contacts reach the app. The default,
"direct", forwards every contact to the app's own wl_touch, so pinch,
rotate, and multi-finger gestures belong to the app. Set "pointer" to opt out
and use Blit's compatibility gestures: tap to click, one-finger drag to scroll,
and long-press for right-click. A server without multitouch support
automatically keeps the pointer mapping. The mode is safe to change at runtime
and does not restart the video stream. Trackpads and pens are unaffected.
<BlitSurfaceView
connectionId={connectionId}
surfaceId={surfaceId}
touchMode="pointer"
/>Every terminal has an experimental Wayland compositor available. Any command — shell, TUI, or GUI — can open Wayland surfaces:
workspace.createSession({
connectionId: "default",
rows: 24,
cols: 80,
command: "my-gui-app",
});Surfaces created by the terminal appear in the connection's surfaceStore, keyed by the terminal's PTY ID. Each surface has a surfaceId, parentId, title, appId, width, and height.
createSession({ connectionId, rows, cols, tag?, command?, cwdFromSessionId? })closeSession(sessionId)restartSession(sessionId)focusSession(sessionId | null)search(query, { connectionId? })setVisibleSessions(sessionIds)addConnection(...)/removeConnection(connectionId)/reconnectConnection(connectionId)
All transports share a common set of options (BlitTransportOptions):
| Option | Default | Description |
|---|---|---|
reconnect |
true |
Auto-reconnect on disconnect |
reconnectDelay |
500 |
Initial reconnect delay (ms) |
maxReconnectDelay |
10000 |
Maximum reconnect delay (ms) |
reconnectBackoff |
1.5 |
Backoff multiplier |
connectTimeoutMs |
none (WS) / 10000 (WT, WebRTC) |
Connection timeout (ms) |
// WebSocket
new WebSocketTransport(url, passphrase, { reconnect, reconnectDelay, connectTimeoutMs, ... })
// WebTransport (QUIC/HTTP3)
new WebTransportTransport(url, passphrase, { serverCertificateHash, ... })
// WebRTC data channel
createWebRtcDataChannelTransport(peerConnection, { label, displayRateFps, ... })Or implement your own:
interface BlitTransport {
connect(): void;
send(data: Uint8Array): void;
close(): void;
readonly status: ConnectionStatus;
readonly authRejected: boolean;
readonly lastError: string | null;
addEventListener(type: "message" | "statuschange", listener: Function): void;
removeEventListener(
type: "message" | "statuschange",
listener: Function,
): void;
}You can also run a @blit-sh/core client server-side (Node/Bun/Deno) to drive a
local blit server over its unix-domain socket — e.g. to script terminals or run
headless commands. The non-browser building blocks live under the
@blit-sh/core/node subpath (kept out of the package root so node:net and
runtime globals never leak into browser bundles):
import { BlitWorkspace, exitCodeFromStatus, nullLogger } from "@blit-sh/core";
import { NodeUnixSocketTransport, loadBlitWasm } from "@blit-sh/core/node";
// `loadBlitWasm()` initializes the @blit-sh/browser WASM off-browser: it reads
// the colocated blit_browser_bg.wasm from disk and feeds it to init(), so you
// never touch raw wasm bytes. (If you depend on a self-initializing
// `@blit-sh/browser/node` build it is returned as-is.)
const wasm = await loadBlitWasm();
const transport = new NodeUnixSocketTransport(
process.env.BLIT_SOCK ?? "/tmp/blit.sock",
);
const workspace = new BlitWorkspace({
wasm,
logger: nullLogger, // no-op logger; omit to log lifecycle events to console
connections: [{ id: "default", transport }],
});
const session = await workspace.createSession({
connectionId: "default",
rows: 24,
cols: 80,
command: "my-command",
});The unix transport speaks blit's framing protocol (4-byte little-endian
length-prefixed frames) for you — there is no need to re-implement the wire
format. BunUnixSocketTransport and DenoUnixSocketTransport are the
runtime-native equivalents.
When a session's process exits, its BlitSession.state becomes "exited" and
BlitSession.exitStatus carries the raw status from the server:
>= 0— normal exit; the value is the exit code.< 0— terminated by a signal; the value is the negated signal number.EXIT_STATUS_UNKNOWN— not yet collected.
exitCodeFromStatus(status) maps that to a conventional shell exit code
(unknown → 1, signalled → 128 + signal), and formatExitStatus(status)
renders "exited(<code>)" / "signal(<n>)". Both mirror the blit CLI.
import { exitCodeFromStatus } from "@blit-sh/core";
workspace.subscribe(() => {
for (const s of workspace.getSnapshot().sessions) {
if (s.state === "exited" && s.exitStatus !== null) {
console.log(`${s.id} exited with code`, exitCodeFromStatus(s.exitStatus));
}
}
});fd-channel lets an external process own blit server's lifecycle and control which clients connect via SCM_RIGHTS fd passing. See ARCHITECTURE.md for the protocol details and the working examples: