A `client:*` directive on a component tells the islands plugin to hydrate it — and when. Components without a `client:*` directive render to static HTML and ship no client JavaScript.
The attributes need no import to type-check — installing the pack is enough. See
[Installation](/server/packages/ssr-islands/installation).
## The strategies
```tsx
` placeholder in its place — no server HTML for the component
itself. The island still gets a boundary record in `__SIGX_BOUNDARIES__` (so the
client knows to mount it), but with **no captured signal `state`**, since nothing
ran server-side. On the client it mounts **fresh**.
Use it for components that cannot render on the server — ones that read `window`,
`localStorage`, or other browser-only globals during setup.
Contrast with `client:load`, which **does** render on the server (you get the SSR
HTML immediately) and then hydrates that existing DOM, resuming from the captured
signal state. Reach for `client:only` only when server rendering is impossible or
unwanted; otherwise a server-rendered strategy gives a faster first paint.
## The strategy type
The six strategies are the `HydrationStrategy` union, and the directives are the
`ClientDirectives` interface that augments component attributes:
```ts
type HydrationStrategy = 'load' | 'idle' | 'visible' | 'media' | 'interaction' | 'only';
interface ClientDirectives {
'client:load'?: boolean;
'client:idle'?: boolean;
'client:visible'?: boolean;
'client:media'?: string; // the media query
'client:interaction'?: boolean;
'client:only'?: boolean;
}
```
### Directives vs. boundary axes
These directives are the islands pack's friendly surface over the two [boundary
axes](/server/packages/server-renderer/boundaries). The two vocabularies don't
line up one-to-one, and it's worth knowing why: `client:only` is a **flush**
concern — it maps to `{ flush: 'skip', hydrate: 'load' }` — while the boundary
model's `hydrate: 'never'` (server HTML, no client component) has no directive of
its own. So don't read the directive list and the `hydrate` axis as the same enum.
## Cleaning up on SPA navigation
Deferred strategies register observers and listeners (`IntersectionObserver`,
`matchMedia`) for islands that have not triggered yet. When you navigate away in
an SPA before they fire, call `cleanupPendingHydrations()` to tear those down and
avoid leaks:
```ts
import { cleanupPendingHydrations } from '@sigx/ssr-islands';
router.beforeEach(() => {
cleanupPendingHydrations();
});
```
## Next steps
- [Registry & code splitting](/server/packages/ssr-islands/registry-and-code-splitting) — make islands' components resolvable on the client.
- [Plugin setup](/server/packages/ssr-islands/plugin-setup) — register the server plugin and transfer signal state.
- [API reference](/server/packages/ssr-islands/api)
---
url: https://sigx.dev/server/packages/server-renderer/installation/
title: Installation
description: Install @sigx/server-renderer, its dependency, and the subpath import map
---
# Installation
Add `@sigx/server-renderer` to your project and import from the right subpath.
## Install
```bash
pnpm add @sigx/server-renderer
```
## Dependency
The only runtime dependency is **`sigx`**, the host framework. It is declared as a
regular dependency, so installing `@sigx/server-renderer` pulls `sigx` in
automatically — there is no separate install step and no peer dependency to add.
The package is **ESM-only** (`"type": "module"`, no CommonJS build). Use it from
an ESM project or bundler.
## Subpath imports
`@sigx/server-renderer` ships four tree-shakeable entry points (all with
`sideEffects: false`). Import from the right one so a browser or edge bundle never
pulls in Node code:
| Import path | Use it for | Notes |
|---|---|---|
| `@sigx/server-renderer` | `createSSR`, `renderDocument`, `useResponse`, `ssrClientPlugin`, `renderHeadToString`, shared types | Universal, WinterCG-clean — plugin system + convenience re-exports |
| `@sigx/server-renderer/server` | `renderDocument*`, `renderToString`, `renderToStream`, `renderToStreamWithCallbacks` | Web-stream render APIs; WinterCG-clean (runs on the edge) |
| `@sigx/server-renderer/node` | `renderToNodeStream`, `renderDocumentToNodeStream`, `toNodeStream`, `createRequestHandler` | Node-only — anything touching `node:stream` |
| `@sigx/server-renderer/client` | `hydrate`, `ssrClientPlugin`, plugin hooks | Browser-only hydration |
The root and `/server` entries import **no Node built-ins**, so they run on edge
runtimes. Everything that needs `node:stream` — a `Readable`, or the Node request
handler — lives in `/node`.
A typical app uses several across its entry files:
```tsx
// entry-server.tsx
import { renderDocument } from '@sigx/server-renderer/server';
// server.mjs (Node) — a Readable stream, or the request handler
import { renderToNodeStream, createRequestHandler } from '@sigx/server-renderer/node';
// entry-client.tsx
import { ssrClientPlugin } from '@sigx/server-renderer/client';
// any component
import { useHead, useData } from 'sigx'; // head + data loading live in core sigx
```
Node stream APIs (`renderToNodeStream`, `renderDocumentToNodeStream`, `toNodeStream`)
are exported **only** from `/node`. Hydration internals (`hydrate`, `hydrateNode`,
`registerClientPlugin`, …) are exported **only** from `/client`.
> `useHead`, `useData`, and `useStream` come from core **`sigx`**, not this
> package. They are plain composables that gain server behavior when this renderer
> drives them.
## Import side effects
Importing the package runs one-time setup automatically: it augments the SSR
directive types and patches `getSSRProps` onto built-in directives such as `show`
so they serialize correctly during SSR. You do not need to call anything to enable
this.
## Build / Vite wiring
No special Vite config is required to **consume** the package — the standard
SignalX JSX/Vite setup for your host app applies. There is **no CLI or `bin`** —
the package is a library you import.
## Adding islands
`@sigx/server-renderer` has no concept of `client:*` directives on its own. To
hydrate only the interactive parts of a page, add the islands package on top:
```bash
pnpm add @sigx/ssr-islands
```
See [`@sigx/ssr-islands`](/server/packages/ssr-islands/overview) for the full setup.
## Next steps
- [Rendering & streaming](/server/packages/server-renderer/rendering)
- [Hydration & head](/server/packages/server-renderer/hydration)
- [API reference](/server/packages/server-renderer/api)
---
url: https://sigx.dev/terminal/packages/terminal-zero/installation/
title: Installation
description: Install and configure @sigx/terminal-zero, the Terminal Zero package for SignalX Terminal.
---
# Installation
Add `@sigx/terminal-zero` to your project.
## Install the package
```bash
pnpm add @sigx/terminal-zero
```
## Verify
```tsx
import * as TerminalZero from '@sigx/terminal-zero';
console.log(Object.keys(TerminalZero));
```
---
url: https://sigx.dev/actors/packages/actors-monitor/installation/
title: Installation
description: Choosing a MonitorSource, running the poll loop with DashboardState, and the rules for writing a renderer over @sigx/actors-monitor
---
# Installation
A source, a state, and a renderer that reads state.view. The
first two are here; the third is yours.
## Install
```bash
pnpm add @sigx/actors-monitor
```
`@sigx/reactivity` is the one runtime peer. `@sigx/actors` is optional and types-only, so
this installs cleanly into an admin portal that has no actor runtime of its own.
## Pick a source
**`httpSource`** polls a running host's [`ops()`](/actors/docs/ops-endpoint) endpoint with
two GETs — `/_sigx/ops` and `/_sigx/ops/cluster`. It loads no user code and holds no host,
so it is the mode for anything you did not just start yourself.
```ts
import { httpSource } from '@sigx/actors-monitor';
// Server-side (Node, a CLI, a worker): the bearer may be supplied here.
const source = httpSource({ url: 'http://actors-host:7311', secret: process.env.SIGX_OPS_SECRET });
// Browser: a same-origin route of YOUR app, and no secret — see below.
const source = httpSource({ url: location.origin, base: '/admin/ops' });
```
| Option | Default | Meaning |
|---|---|---|
| `url` | — | origin of the host's mount (or of your proxy) |
| `secret` | — | the `ops({ secret })` bearer — **server-side only** |
| `base` | `/_sigx/ops` | path prefix, matching `ops({ base })` |
| `timeoutMs` | `5000` | per-request budget |
| `fetch` | `globalThis.fetch` | injectable for tests |
A failed request rejects with `OpsRequestError`, carrying the HTTP `status` (or `null` for a
transport failure) — a 401 means the host answered and rejected the secret, a 404 that it
serves no ops endpoint. Neither is a reachability problem, and the message says which.
**`embeddedSource`** loads your app module in-process and **starts a real host**. It lives
only in [`@sigx/actors-cli/source`](/actors/packages/actors-cli/api), because it
dynamic-imports user code and needs Node — a bundler following `@sigx/actors-monitor` cannot
reach it at all.
## The same-origin proxy
`ops()` sets no CORS headers and refuses to construct without a bearer secret outside dev.
A browser therefore cannot call it cross-origin, and must not be handed the secret to try.
Point `httpSource` at a route of your own app that authenticates the operator and forwards
with the bearer attached server-side:
```js
// GET /admin/ops → the host snapshot
// GET /admin/ops/cluster → the fan-out
if (url.pathname === '/admin/ops' || url.pathname.startsWith('/admin/ops/')) {
if (!(await isOperator(request))) return new Response('no', { status: 403 });
return fetch(HOST_ORIGIN + url.pathname.replace('/admin/ops', '/_sigx/ops') + url.search, {
headers: { authorization: `Bearer ${process.env.OPS_SECRET}` }
});
}
```
Forward the sub-path **and** the query verbatim — `httpSource` appends `/cluster` and
`?detail=1&host=…` itself, and a proxy that drops either breaks the cluster view or the
per-host drill-down with no error anywhere. The full discussion, and the two ways the proxy
fails silently, is on the [ops endpoint page](/actors/docs/ops-endpoint#reaching-ops-from-a-browser).
## Run the poll loop
```ts
import { DashboardState, httpSource } from '@sigx/actors-monitor';
const state = new DashboardState({ source, intervalMs: 1000, history: 60 });
state.start();
// …
await state.stop(); // aborts the in-flight poll and closes the source
```
`state.view` is one deep signal — `{ snapshot, error, paused, intervalMs, lastOk, polls,
focus }` — and `state.calls`, `state.failures`, `state.queued` and `state.activations` are
the derived `Series` a sparkline reads. Three behaviours are already decided so you do not
have to re-decide them:
- **The last good snapshot survives a failed poll.** `error` is set and cleared by the next
success; `snapshot` is kept, because a dashboard that blanks the moment a host hiccups
destroys exactly the context you need to understand the hiccup. Show `lastOk`'s age so
stale data never looks live.
- **Back-pressure abandons, never queues.** A poll still in flight when the next tick fires is
aborted rather than stacked behind a slow host.
- **A drill-down changes what is requested.** `state.focus(hostId)` switches the poll to
`{ detail: true, hostId }` and polls immediately; `focus(null)` closes it. A detail poll
makes that host walk its activation table, so nobody should pay for a panel that is closed.
`intervalMs` is clamped to 200 ms – 60 s; `nudgeInterval(factor)` steps it and
`togglePause()` pauses without stopping.
## Writing a renderer
1. Take a `MonitorSource`, or a `DashboardState` built over one. Do not poll `ops()`
yourself.
2. Use `alertLines`, `scopeOf`, `polledLabel`, `coverageNote`, `shardStates` and
`percentilePoints` as given, and map their severities into your own vocabulary — the
mapping is about ten lines.
3. Anything you find yourself deriving that is about *actors* rather than about *drawing*
belongs in the monitor, not in your renderer.
The two shipped renderers are the worked examples:
[`@sigx/actors-cli`](/actors/packages/actors-cli/overview) for a terminal and
[`@sigx/actors-dashboard`](/actors/packages/actors-dashboard/overview) for a browser.
## Next steps
- [API reference](/actors/packages/actors-monitor/api) — every export.
- [The ops endpoint](/actors/docs/ops-endpoint) — what is being polled.
- [Cluster stats](/actors/docs/cluster-stats) — `partial`, and why latency merges rather than averages.
---
url: https://sigx.dev/actors/packages/actors-surreal/installation/
title: Installation
description: Install and configure @sigx/actors-surreal, the SurrealDB package for SignalX.
---
# Installation
Connect, define the schema, then wire a clustered host.
## Install
```bash
pnpm add @sigx/actors-surreal surrealdb
```
`surrealdb` is a peer dependency (`^2.0.8`). You need SurrealDB **≥ 3.0** running, 3.2.4 or
newer recommended.
## Connect and define the schema
```ts
import { Surreal } from 'surrealdb';
import { ensureSurrealSchema, surrealRetryable } from '@sigx/actors-surreal';
const db = new Surreal();
await db.connect('ws://127.0.0.1:8000', {
namespace: 'app',
database: 'main',
authentication: { username: 'root', password: 'root' },
// REQUIRED on a connection you own — see below.
retry: { enabled: true, attempts: 5, retryable: surrealRetryable },
});
await ensureSurrealSchema(db);
```
**Every replica may call this at boot, concurrently.** SurrealDB 3 has no lock primitive, so
convergence is by a bounded, jittered retry that `ensureSurrealSchema()` carries **itself**,
independent of the connection's `retry` setting — which matters, because the SDK ships retry
disabled and this path would otherwise have none. The retry is deliberately blind to error
shape; if it exhausts its attempts it verifies the tables are present before giving up, and
rethrows the original error if they are not.
`surrealRetryable` is unchanged by any of that and stays deliberately narrow — it is a
connection-wide predicate governing your own queries, and the bootstrap does not depend on it.
`ensureSurrealSchema()` SELECTs the namespace and database; it does not create them.
`DEFINE NAMESPACE` / `DEFINE DATABASE` need root and are a deployment decision, so they are
deliberately not issued for you.
Two things in that snippet are load-bearing:
surrealRetryable is not optional on a connection you pass in.
The directory claim and the storage create arm are correct because two racers
collide at commit and the loser re-runs to observe the winner. The SDK ships retry
disabled, and its built-in predicate matches a structured error code that in practice never
arrives — so without this, a lost claim race surfaces as a raw conflict error instead of
the winning entry.
The DDL step is mandatory, unlike with Postgres. Reading an undefined
table is an error in SurrealDB 3 (2.x returned []), so the schema has
to exist before a host starts.
Prefer `ws://`/`wss://` over `http://`: the HTTP engine re-authenticates per request and cannot
serve live queries, so membership push will not work over it.
### In production, use a migration tool
Calling `ensureSurrealSchema()` from every replica is safe, but a migration tool is still the
better shape in production — it runs once, under review, rather than racing at every boot:
```ts
import { surrealSchemaSql } from '@sigx/actors-surreal';
console.log(surrealSchemaSql({ prefix: 'sigx_' }));
```
The DDL is idempotent and safe to re-run. Every table is `SCHEMAFULL` — this package is the
only writer and all five shapes are fixed, so a typo becomes an error at the write rather than
a silently ignored field. That works because a v3 `SCHEMAFULL` table **rejects** an undefined
field instead of dropping it.
Because the providers never issue DDL, a production role needs only DML grants.
## Wire a host
```ts
import { defineActorApp } from '@sigx/actors/host';
import { cluster } from '@sigx/actors/cluster';
import { surrealCluster, surrealReminders, surrealStorage } from '@sigx/actors-surreal';
const app = defineActorApp({
actors,
storage: surrealStorage({ db }),
// Optional: without it the runtime keeps its default sharded reminders,
// which also work over surrealStorage. Pass it to get the indexed table.
reminders: surrealReminders({ db }),
}).use(
cluster({
providers: surrealCluster({ db }),
advertise: process.env.ADVERTISE!,
secret: process.env.CLUSTER_SECRET!,
}),
);
```
## Connection: shared or owned
Every provider takes one of two shapes:
| You pass | What happens |
|---|---|
| `db` — a connected `Surreal` | Shared with your app; **one socket multiplexes everything**. You own the retry config. |
| `url` plus `namespace` / `database` / `auth` | The package connects lazily and owns the socket, retry included. |
`prefix` (default `sigx_`) names the tables.
## Verify
```ts
import { surrealStorage } from '@sigx/actors-surreal';
const storage = surrealStorage({ db });
await storage.save('Probe', 'k1', { state: '{"n":1}', etag: 'e1' });
console.log(await storage.load('Probe', 'k1')); // → { state: '{"n":1}', etag: 'e1' }
await storage.clear('Probe', 'k1');
```
A `load` that returns `undefined` immediately after a `save` usually means the schema step did
not run against this namespace/database.
---
url: https://sigx.dev/terminal/packages/runtime-terminal/overview/
title: Overview
description: "@sigx/runtime-terminal — the terminal renderer for SignalX: render modes, key dispatch, color depth"
---
# Runtime Terminal
The renderer. Walks your component tree into ANSI lines and paints them — render modes, layered key dispatch, color-depth detection, output targets and reactive terminal size. The host platform for `@sigx/runtime-core`.
MIT
The `@sigx/terminal` umbrella re-exports this package, so most apps never install it
directly — it is to the terminal what `@sigx/runtime-dom` is to the web:
```bash
pnpm add @sigx/runtime-terminal
```
## What lives here
- **Render modes** — fullscreen (alternate screen), inline live regions and
one-shot static rendering, plus the mount options that pick between them. See
[Render modes](/terminal/docs/render-modes/).
- **Layered key dispatch** — the focus model interactive components plug into. See
[Input & interactive components](/terminal/docs/input-and-components/).
- **Terminal capabilities** — color-depth detection, output targets and the
reactive terminal size the layout engine tracks.
## Next steps
See where it sits in the stack in [Architecture](/terminal/docs/architecture/), or
jump to the [API reference](/terminal/packages/runtime-terminal/api).
---
url: https://sigx.dev/terminal/packages/terminal-dev/installation/
title: Installation
description: Install and configure @sigx/terminal-dev, the Terminal Dev package for SignalX Terminal.
---
# Installation
Add `@sigx/terminal-dev` to your project as a dev dependency.
## Install the package
It runs your app during development only — it is not part of the `@sigx/terminal` umbrella and never ships with your app:
```bash
pnpm add -D @sigx/terminal-dev
```
## JSX setup
`@sigx/terminal-dev` lists `@sigx/runtime-core` (`^0.14.0`) as a peer dependency and bundles Vite. Your `tsconfig.json` needs the usual SignalX JSX setup (`"jsx": "react-jsx"` and `"jsxImportSource": "@sigx/runtime-core"`, or the `@sigx/terminal` facade — see the umbrella's [Installation](/terminal/docs/installation/) guide).
## Verify
```bash
pnpm exec sigx-terminal-dev --help
```
---
url: https://sigx.dev/actors/packages/actors-cloudflare/overview/
title: Overview
description: Cloudflare Durable Objects as the backend for @sigx/actors — one DO per actor, and why the package is so small
---
# Cloudflare
Cloudflare already guarantees a single instance of a Durable Object globally
and serializes requests to it. That is the virtual-actor contract — so the platform
is the cluster.
MIT
## Installation
```bash
pnpm add @sigx/actors-cloudflare
```
## Why it is small
This package needs none of the machinery [`@sigx/actors/cluster`](/actors/docs/clustering)
uses to rebuild that contract: **no membership heartbeats, no activation directory, no
HMAC-authenticated host-to-host mount.**
There is no HMAC because a Durable Object stub is not network-reachable — holding the binding
*is* the capability grant, and guards run once at the public edge. And there is no 421 retry
because ref → object id is a pure function, so a mismatch is a config bug rather than a race.
## Storage
```ts
const storage = durableObjectStorage(state.storage);
```
DO storage is strongly consistent and single-threaded per object, so the runtime's etag
compare-and-set holds without a transaction.
## Reminders
```ts
const reminders = durableObjectReminders({
storage: state.storage,
alarms: state.storage,
blockConcurrencyWhile: (fn) => state.blockConcurrencyWhile(fn),
});
export class ActorHost {
async alarm() {
await reminders.onAlarm(); // fire what is due, re-arm the rest
}
}
```
The default `shardedReminders()` splits one table into fixed hash shards and polls it, because
a host holds many actors and has to find whose reminder is due. **A DO holds exactly one**, so
there is nothing to search and nothing to poll — reminders live in the object's own storage and
the platform wakes it at the earliest due time.
The visible consequence: an alarm fires **at** the due time, where `shardedReminders()`
promises only "at or after `nextDue`".
## Client sockets
Browsers can reach actors over a WebSocket here, terminating in either half:
```ts
createWorkerHandler({ ..., socket: {} }); // in the Worker
createWorkerHandler({ ..., socket: { terminate: 'object' } }); // in the object
```
**Worker-terminated** gives one multiplexed socket per client, reaching every actor — the
right shape for a dashboard. **Object-terminated** gives one socket per actor, accepted with
the hibernation API inside the object that owns it — the room pattern, and the mode where a
disconnect actually releases the activation, because teardown happens locally instead of dying
at the `stub.fetch` boundary.
Pair either with [`socketTransport()`](/actors/packages/actors-ws/overview) on the client. The
[deployment guide](/actors/docs/cloudflare-workers#client-sockets) has the choice in full,
plus the hibernation contract.
## The getting-started path
Most apps do not touch the two seams directly — `createHostDurableObject()` and
`createWorkerHandler()` assemble them. See
[Cloudflare Workers](/actors/docs/cloudflare-workers) for the full entry, the `wrangler.jsonc`
and the three gotchas that are each worth their own callout.
## Next steps
- [Installation](/actors/packages/actors-cloudflare/installation) — bindings and migrations.
- [API reference](/actors/packages/actors-cloudflare/api) — exports.
- [Cloudflare Workers](/actors/docs/cloudflare-workers) — the deployment guide.
- [`@sigx/actors-ws`](/actors/packages/actors-ws/overview) — the client half of a socket.
---
url: https://sigx.dev/server/packages/server-renderer/boundaries/
title: The boundary model
description: How SignalX SSR decides where HTML flushes and when a component hydrates — flush and hydrate axes, SSRBoundary, resolveBoundary, and selective hydration
---
# The boundary model
A boundary is a component the renderer treats specially: it decides how that component's HTML reaches the page and when the component wakes up on the client. It's the strategy-agnostic core of SignalX SSR — islands are one pack built on it.
## Two orthogonal axes
Every boundary is described by two independent choices. Keeping them separate is the whole point of the model — a server-flush decision never dictates a client-hydration decision.
**`flush`** — a **server** concern: how the component's HTML gets to the page.
| `flush` | Meaning |
|---|---|
| `inline` | Await the component's async work in place, even in a streaming render. |
| `stream` | Stream the HTML when there is pending async work and the render is streaming; otherwise degrade to inline. |
| `skip` | Don't run setup on the server at all — emit a placeholder wrapper and let the client mount fresh. |
**`hydrate`** — a **client** concern: when the component becomes interactive.
| `hydrate` | Wakes on |
|---|---|
| `load` | As soon as the bundle loads. |
| `idle` | The next idle callback. |
| `visible` | Scrolled into view. |
| `media` | A media query matches (see `media`). |
| `interaction` | The first pointer/keyboard/touch/focus event on the element. |
| `never` | Never — server HTML only, no client component. |
A static marketing section might be `{ flush: 'inline', hydrate: 'never' }`; a below-the-fold widget `{ flush: 'stream', hydrate: 'visible' }`; a client-only chart `{ flush: 'skip', hydrate: 'load' }`.
## Describing a boundary
`SSRBoundary` is the resolved description the renderer works from:
```ts
interface SSRBoundary {
id: number; // renderer-assigned, from core's component-id scheme
flush: BoundaryFlush; // 'inline' | 'stream' | 'skip'
hydrate: BoundaryHydrate; // 'load' | 'idle' | 'visible' | 'media' | 'interaction' | 'never'
media?: string; // required when hydrate is 'media'
fallback?: () => JSXElement; // server-only placeholder for 'stream' / 'skip'; never serialized
chunk?: { url: string; export?: string };
props?: Record
;
}
```
You rarely write this by hand — a pack (like islands) produces it from something friendlier, such as a `client:*` directive.
## Resolving a boundary
Boundaries come from a plugin's `resolveBoundary` hook. The renderer calls it once per component — **after it allocates the component's id, before setup runs** — and the first plugin to return an object wins:
```ts
const myBoundaryPlugin: SSRPlugin = {
server: {
resolveBoundary(vnode, ctx) {
if (vnode.type?.__deferred) {
return { flush: 'stream', hydrate: 'visible' };
}
// return nothing → this component is not a boundary
},
},
};
```
The returned object is a `ResolvedBoundary` — a partial of the `flush` / `hydrate` / `media` / `fallback` / `chunk` / `props` / `component` fields. Anything omitted falls back to the app's defaults.
`component` is the record's **registry name** — how the client resolves the component, trying the eager registry, then the lazy registry, then the `chunk` URL, in that order. Core derives it from `__islandId || __name`, which is right for packs that use core's stamp; a pack with its own naming vocabulary sets it here instead. An anonymous record — no `component` and no `chunk` — is refused by the client loader rather than silently skipped.
Because the hook runs before setup, a `flush: 'skip'` decision means setup never runs on the server: the renderer emits a `` wrapper around the optional `fallback` and the client mounts into it fresh. For `stream` and `inline`, setup runs normally and the boundary's HTML is produced server-side.
## The client's view: `__SIGX_BOUNDARIES__`
Every recorded boundary is serialized into a per-request table emitted as `window.__SIGX_BOUNDARIES__`. Each entry carries only what the client needs — the resolved `hydrate` strategy, any `props` or transferred signal `state`, the `chunk` reference for a lazy boundary, and an `errorScope` marker if the server caught an error there:
```ts
interface SSRBoundaryRecord {
flush?: BoundaryFlush; // present only when 'skip'
hydrate?: BoundaryHydrate; // omitted = inherit the app default
media?: string;
props?: Record
;
state?: Record; // transferred island signal snapshot
chunk?: { url: string; export?: string };
component?: string;
errorScope?: { message: string };
}
```
The table shares one serializer (and one dev-time serializability warning) with `window.__SIGX_ASYNC__`, so the same escaping and custom-type rules apply to both. That serializer is **codec-aware**: alongside JSON primitives, top-level and nested `Date` / `Map` / `Set` / `bigint` / `URL` / `RegExp` / explicit `undefined` values — plus any registered custom type handlers — round-trip; only functions and circular references are rejected.
## Custom types
The codec ships with handlers for `Date`, `Map`, `Set`, `bigint`, `URL`, `RegExp` and `undefined`. To carry a type of your own across the boundary — the SSR state blob, the boundary table, and server-function arguments and results — register a handler with [`@sigx/serialize`](/server/packages/serialize/overview)'s `defineTypeHandler`, most easily through the `@sigx/server/plugin` app plugin's `types` option:
```ts
import { serverPlugin } from '@sigx/server/plugin';
app.use(serverPlugin({ types: [moneyHandler] }));
```
See [Serialize → Usage](/server/packages/serialize/usage) for writing the handler (its `test` type guard drives the `serialize` / `revive` types) and the other registration paths.
## Selective hydration is the hydrator
On the client, `hydrate()` reads the boundary table and schedules work per strategy — this *is* the hydrator, not an add-on. Its behavior is governed by a per-app hydrate default:
- **`explicit`** — only entries in the boundary table are scheduled; there is no root walk. A page with no table pays nothing.
- **`auto`** — the client walks the tree and intercepts recorded boundaries as it finds them.
Installing a pack is what selects the mode. `app.use(islandsPlugin())` provides the `explicit` default and registers the client hooks — so *installing* the package is what turns on islands semantics, never merely importing it.
## Where to go next
- [Islands & client directives](/server/packages/ssr-islands/client-directives) — the reference pack that maps `client:*` onto these axes
- [Request lifecycle](/server/packages/server-renderer/request-lifecycle) — `useResponse`, the error seam, and the `./node` entry
- [Building a full SSR app](/server/packages/server-renderer/full-app) — wiring it all together
---
url: https://sigx.dev/terminal/packages/terminal-ui/installation/
title: Installation
description: Install and configure @sigx/terminal-ui, the Terminal UI package for SignalX Terminal.
---
# Installation
Add `@sigx/terminal-ui` to your project.
## Install the package
```bash
pnpm add @sigx/terminal-ui
```
## Verify
```tsx
import * as TerminalUI from '@sigx/terminal-ui';
console.log(Object.keys(TerminalUI));
```
---
url: https://sigx.dev/actors/packages/actors-dashboard/overview/
title: Overview
description: The web dashboard for @sigx/actors — hosts, actors, latency, errors and cluster topology as embeddable sigx components, over a same-origin ops proxy
---
# Dashboard
The five tabs sigx actors top has, in a browser: Overview, Hosts
(with a per-host drill-down), Actors, Cluster and Health — as sigx components you drop into
your own admin portal.
MIT
## Installation
```bash
pnpm add @sigx/actors-dashboard @sigx/actors-monitor
```
```tsx
import { ActorsDashboard } from '@sigx/actors-dashboard';
import { httpSource } from '@sigx/actors-monitor';
```
That is the whole integration. The component owns a `DashboardState` for its lifetime, polls
once a second, and stops on unmount — which matters more in a browser than in a terminal,
because a single-page app that navigates away from an unstopped dashboard leaves it polling
the cluster forever.
Peers: `@sigx/actors`, `@sigx/runtime-core`, `@sigx/runtime-dom` and `@sigx/reactivity`.
The one real dependency is [`@sigx/actors-monitor`](/actors/packages/actors-monitor/overview),
which decides what every number means; this package renders its verdicts and re-derives none
of them. That is what keeps it from disagreeing with the CLI: same data layer, two renderers.
## `/admin/ops` is a route of your app, not the host's
Never put the ops secret in the browser. ops() sets no CORS
headers and refuses to construct without a bearer secret outside dev — it reports your
actor type names, traffic shape and cluster topology, and the activation list carries actor
keys, which are user data. Work around the CORS block by passing
secret to httpSource in browser code and you have published your
cluster topology to every visitor, and nothing in the browser will tell you.
So the browser calls a **same-origin route of your own app**, which authenticates the operator
however your app already does and forwards to the host with the bearer attached server-side.
The server half is about nine lines:
```js
// GET /admin/ops → the host snapshot
// GET /admin/ops/cluster → the fan-out
if (url.pathname === '/admin/ops' || url.pathname.startsWith('/admin/ops/')) {
if (!(await isOperator(request))) return new Response('no', { status: 403 });
return fetch(HOST_ORIGIN + url.pathname.replace('/admin/ops', '/_sigx/ops') + url.search, {
headers: { authorization: `Bearer ${process.env.OPS_SECRET}` }
});
}
```
Forward the sub-path **and** the query verbatim, and match the mount exactly — both failure
modes are silent, and both are spelled out on the
[ops endpoint page](/actors/docs/ops-endpoint#reaching-ops-from-a-browser).
## Embed one panel, not the shell
An admin portal that wants one table — a tenant's hosts, say — builds the state itself and
renders the panel it needs. Every panel is exported standalone and takes `{ state }`:
```tsx
import { DashboardState, HostsPanel, httpSource } from '@sigx/actors-dashboard';
const state = new DashboardState({ source: httpSource({ url: location.origin, base: '/admin/ops' }) });
state.start();
```
`OverviewPanel`, `HostsPanel`, `HostPanel`, `ActorsPanel`, `ClusterPanel` and `HealthPanel`
compose in any order over one state. Stop the state yourself when the page tears down.
## Theming
Styling is self-contained and arrives with the component — nothing to import, no CSS
framework. Every colour and metric is a `--sigx-actors-*` custom property, so a portal
restyles it by overriding tokens on any ancestor, without overriding a single rule:
```css
.my-portal {
--sigx-actors-accent: #7c3aed;
--sigx-actors-font: Inter, system-ui, sans-serif;
}
```
It follows `prefers-color-scheme`; `theme="light"` or `"dark"` forces a palette. For a strict
CSP or a build that extracts CSS, pass `styles={false}` and ship the exported
`actorsDashboardCss` yourself.
Two tokens carry meaning rather than decoration. `--sigx-actors-danger` and
`--sigx-actors-warn` are never confusable, because an unclaimed reminder shard is an incident
and a doubly-claimed one is a divergence. And `--sigx-actors-gap` is the colour of "no
reading": a counter reset must look like missing data, not like quiet traffic.
## Next steps
- [Installation](/actors/packages/actors-dashboard/installation) — mounting outside a sigx app, and writing your own panel.
- [API reference](/actors/packages/actors-dashboard/api) — props, panels and the drawing parts.
- [`@sigx/actors-monitor`](/actors/packages/actors-monitor/overview) — the data layer underneath.
- [The CLI](/actors/packages/actors-cli/overview) — the same five tabs in a terminal.
---
url: https://sigx.dev/actors/packages/actors-redis/installation/
title: Installation
description: Install @sigx/actors-redis, wire the providers into your app, and tune the heartbeat
---
# Installation
Add the package and its ioredis peer, then hand the providers to
the cluster() plugin.
## Install
```bash
pnpm add @sigx/actors-redis ioredis
```
Requires **Redis ≥ 7** and `ioredis` ≥ 5.
## Wire it up
```ts
import Redis from 'ioredis';
import { defineActorApp } from '@sigx/actors/host';
import { cluster } from '@sigx/actors/cluster';
import { redisCluster, redisStorage } from '@sigx/actors-redis';
const client = new Redis(process.env.REDIS_URL!);
export const app = defineActorApp({
actors,
storage: redisStorage({ client }),
}).use(cluster({
providers: redisCluster({ client }),
advertise: `http://${process.env.POD_IP}:7311`,
secret: process.env.HOST_SECRET,
}));
```
One client for both is safe and saves connections. Share the `namespace` between them.
## Options
Both `redisCluster()` and `redisStorage()` take `client` **or** `url`.
| Option | Default | Meaning |
|---|---|---|
| `client` / `url` | — | ioredis client, or a URL to construct one |
| `namespace` | `sigx` | key prefix |
| `heartbeatMs` | `5000` | membership heartbeat cadence |
| `ttlMs` | `15000` | heartbeat key TTL — missed beats past this means dead |
| `pollMs` | `5000` | membership view poll cadence |
`redisStorage()` takes `client`/`url` and `namespace` only.
Tuning guidance: `ttlMs` is how long a dead host's actors stay unclaimable, so lowering it
speeds recovery and raises the risk of fencing a host that merely paused — a long GC, a
throttled container. Three missed heartbeats is a reasonable floor.
## Splitting the providers
Membership and the directory are independent. Kubernetes Leases for liveness with a Redis
directory is a common shape:
```ts
cluster({
providers: { membership: k8sMembership(), directory: redisDirectory(client) },
advertise, secret,
});
```
## Verify
```ts
const report = await clusterStats(placement);
console.log(report.hosts.length, report.partial);
```
Or from a terminal, once [`ops()`](/actors/docs/ops-endpoint) is mounted:
```bash
sigx actors health --url http://localhost:7311
```
## Testing
The provider suite is gated on `REDIS_URL`:
```sh
REDIS_URL=redis://localhost:6379 pnpm test -- actors-redis
```
For unit tests that need a cluster but not a server, `memoryClusterHub()` from
`@sigx/actors/cluster` gives an N-host in-process cluster with no external store.
## Next steps
- [API reference](/actors/packages/actors-redis/api) — exports and key layout.
- [Clustering](/actors/docs/clustering) — the plugin options.
- [Storage](/actors/docs/storage) — choosing a provider.
---
url: https://sigx.dev/server/packages/server-renderer/hydration/
title: Hydration & head
description: Hydrate server-rendered DOM, manage the document head, and extend rendering through the plugin SPI
---
# Hydration & head
After the server sends HTML, the client re-attaches reactivity to that existing DOM. This guide covers the hydration entry point, head management, and the plugin SPI that advanced strategies build on.
## The client hydration entry
The high-level entry is the `ssrClientPlugin`. It adds `hydrate()` to your `App`:
```tsx
import { defineApp } from 'sigx';
import { ssrClientPlugin } from '@sigx/server-renderer/client';
import { App } from './App';
defineApp()
.use(ssrClientPlugin)
.hydrate('#root');
```
`hydrate()` accepts a CSS selector string or an `Element`. It finds the root
component plus `AppContext`, then hydrates the existing SSR DOM. If the container
has **no** SSR content, it falls back to a fresh client render — so the same entry
works for both pre-rendered and client-only pages.
### Gate hydration on the completion script
The document renderer emits a trailing script —
[`__SIGX_STREAMING_COMPLETE__`](/core/docs/advanced/globals) plus
a `sigx:ready` event — in **both** `'blocking'` and `'stream'` modes, so a
blocking-rendered page hydrates the same way a streamed one does. The inline
script runs during HTML parse, before any deferred module script, so your entry
can safely check the flag and otherwise wait for the event before hydrating:
```tsx
// src/entry-client.tsx
import { defineApp } from 'sigx';
import { ssrClientPlugin } from '@sigx/server-renderer/client';
import { App } from './App';
function start() {
defineApp().use(ssrClientPlugin).hydrate('#root');
}
if ((window as any).__SIGX_STREAMING_COMPLETE__) {
start(); // document already complete
} else {
window.addEventListener('sigx:ready', start, { once: true });
}
```
### Self-healing on a structural mismatch
Hydration is resilient to SSR/client drift. For a small cursor offset the walk
scans forward for the matching node (dev-warns about skipped siblings). For a
genuine structural mismatch — the server emitted a different element than the
client VNode expects — it **self-heals**: it creates the expected element fresh,
mounts the subtree into it, and advances past the orphaned server node. The page
stays correct and reactive instead of duplicating content or leaving a dead
subtree. These recoveries warn in dev so you can fix the underlying drift.
### Low-level hydration
For custom setups you can call the core `hydrate` function directly. It normalizes
the element to a VNode, sets the current `AppContext` for DI, runs client plugin
`beforeHydrate` hooks, walks the DOM via `hydrateNode`, then runs `afterHydrate`:
```tsx
import { hydrate } from '@sigx/server-renderer/client';
import { App } from './App';
const container = document.getElementById('root')!;
hydrate(, container);
```
`hydrateNode` does the per-VNode work: in the happy path it creates no DOM, just
attaches events/props/directives/refs, skips SSR comment markers
(``, ``), recovers from minor SSR drift by scanning forward
(with a dev warning), and returns the next sibling.
## Head management
Call `useHead()` — exported from core **`sigx`** — inside any component to manage
`` elements. During SSR the configs are collected onto the per-request
context; on the client `useHead()` mutates the DOM directly and registers cleanup
on unmount:
```tsx
import { component, useHead } from 'sigx';
export const ArticlePage = component((props) => {
useHead({
title: props.title,
titleTemplate: '%s — My Site',
meta: [
{ name: 'description', content: props.summary },
{ property: 'og:title', content: props.title },
],
link: [{ rel: 'canonical', href: props.url }],
htmlAttrs: { lang: 'en' },
});
return () => {props.body};
});
```
`renderDocument*` collects these configs during render and injects the rendered
head HTML before `` in your template automatically — there is nothing to
wire up. `titleTemplate` uses `%s` as the title placeholder, and the renderer
dedupes meta by `name`/`property`/`http-equiv`/`charset`.
`renderDocument` injects the collected head into the document for you, so a
`useHead()` call anywhere in the tree just works — no manual wiring.
## Hydrating a server-resolved Defer subtree
The server resolves `lazy()` components inline (and streams a ``
fallback then swaps in one replacement — see
[Rendering & streaming](/server/packages/server-renderer/rendering)). To hydrate
that server-resolved subtree, the lazy component must be available **synchronously**
during the hydration walk — otherwise the client would render the fallback again
and mismatch the server output.
Preload the lazy chunk before calling `hydrate()`. Every `lazy()` factory exposes
a `.preload()` promise for exactly this:
```tsx
// src/entry-client.tsx
import { defineApp, lazy } from 'sigx';
import { ssrClientPlugin } from '@sigx/server-renderer/client';
import { App } from './App';
import { HeavyChart } from './HeavyChart'; // a lazy() component used under
// Resolve the chunk first, then hydrate — the component is ready when the
// hydration walk reaches it, so it matches the server-rendered DOM.
await HeavyChart.preload();
defineApp().use(ssrClientPlugin).hydrate('#root');
```
For selective hydration where each island owns its own lazy chunk, the islands
plugin handles this for you — see
[`@sigx/ssr-islands`](/server/packages/ssr-islands/registry-and-code-splitting).
## The scheduler / core split
Selective hydration is deliberately in two halves, so a page pays for triggers
before it pays for a renderer.
| Half | Entry | What it costs |
|---|---|---|
| **Eager scheduler** | `@sigx/server-renderer/client/scheduler` | ~2 kB. Reads `__SIGX_BOUNDARIES__` and wires each boundary's trigger. Value-imports nothing from the sigx family, so no framework code executes at load. |
| **Hydration core** | loaded via `loadHydrationCore()` | The renderer, `hydrateComponent`, and the mount/hydrate primitives. Dynamically imported on the **first strategy that actually fires**. |
A page whose strategies never fire — everything below the fold, everything
`hydrate: 'never'` — never executes any framework JavaScript at all.
```ts
import { scheduleTableBoundaries } from '@sigx/server-renderer/client/scheduler';
scheduleTableBoundaries(); // triggers only; the executor arrives when one fires
```
The entry also exports the pieces a strategy pack builds on without pulling the
core in: `scheduleByStrategy`, `getBoundaryTable` / `getBoundaryRecord`,
`findBoundaryMarker` / `hydrateTableBoundary`, the component registry
(`registerComponent`, `resolveComponent`, `registerComponentChunk`),
`loadBoundaryComponent` / `prefetchBoundaryChunks`, and
`seedBoundaryState` / `consumeBoundaryState`.
`loadHydrationCore()` caches its promise, and a failed load clears the cache so
the next trigger retries.
The one behavioural consequence: a `hydrate: 'load'` boundary now hydrates after
one dynamic-import round trip rather than synchronously. That import is
preloadable — a pack keeps it off the critical path with the
[`assets` hook](#the-plugin-spi).
### Lazy client plugins
Because the core loads late, a pack's client hooks can ride in the same chunk.
`registerClientPlugin` takes either a resolved plugin or a **lazy source**:
```ts
import { registerClientPlugin } from '@sigx/server-renderer/client/scheduler';
registerClientPlugin({
name: 'my-strategy',
load: () => import('./my-strategy-client'), // resolved with the hydration core
});
```
`resolveClientPlugins()` imports every lazy source once, before the first
component hydrates, so the synchronous client hooks always see a resolved plugin.
Registrations dedupe by `name`, first wins — registering the same name again in
either form is a no-op.
## The plugin SPI
The core renderer and hydrator are strategy-agnostic. Every advanced hydration
strategy — selective, islands, resumable, `Defer` — is an `SSRPlugin` with
optional `server` and `client` hook sets.
- Register a hand-written `SSRPlugin`'s **server** hooks by passing it to `createSSR({ plugins: [myPlugin] })`. (Published packs like `islandsPlugin()` are `SSRPack`s — an `SSRPlugin` plus an `install(app)` method — so you install *those* on the app with `app.use()`.)
- Register **client** hooks with `registerClientPlugin(plugin)`.
```tsx
import { createSSR, type SSRPlugin } from '@sigx/server-renderer';
const myPlugin: SSRPlugin = {
name: 'my-strategy',
server: {
// decide how this component flushes and hydrates — the per-component seam
resolveBoundary(vnode, ctx) {
return; // no opinion; the next plugin (or the default) decides
},
// mutate/replace a component's context after it's built, before setup()
transformComponentContext(ctx, vnode, componentCtx) {
return; // accept as-is
},
// append-only: capture per-boundary state, emit markup after a component
afterRenderComponent(id, vnode, html, ctx) {
return; // append nothing
},
// contribute modulepreload hints for chunks core won't schedule itself
assets(ctx) {
return; // nothing to preload
},
},
client: {
// return false to skip the default DOM walk (resumable SSR)
beforeHydrate(container) {
return; // run normal hydration
},
// hydration-time mirror of server.transformComponentContext
transformComponentContext(vnode, componentCtx) {
return; // accept as-is
},
// return a Node to "claim" a component during the hydration walk
hydrateComponent(vnode, dom, parent, regionEnd) {
return undefined; // let core hydrate it
},
},
};
const html = await createSSR({ plugins: [myPlugin] }).render();
```
Key hook semantics:
- `server.transformComponentContext` runs after a component's context is built and
**before `setup()`**, letting a plugin mutate or replace it — e.g. swap
`ctx.signal` for a state-capturing variant. `client.transformComponentContext` is
its **hydration-time mirror** (same timing, no `SSRContext` argument), so a
strategy can swap `ctx.signal` for a state-*restoring* variant. The pair keeps
render and hydration symmetric while core stays strategy-agnostic.
- `server.resolveBoundary` runs **before** the context is built and before
`setup()`, once per component, and the first plugin to return an object wins.
Its `flush` axis decides whether the component renders on the server at all —
`flush: 'skip'` suppresses setup entirely and emits the
`` wrapper around an optional
`fallback` — and its `hydrate` axis is recorded in the boundary table for the
client. This is how islands make `client:only` ship no server HTML. See
[The boundary model](/server/packages/server-renderer/boundaries).
- `client.beforeHydrate` returning `false` **skips** the default DOM walk — the
basis for resumable SSR.
- `client.hydrateComponent` returning a `Node` **claims** that component — the hook
islands use to intercept `client:*` props and schedule deferred hydration. Its
fourth argument, `regionEnd`, is the exclusive end of the sibling range the
component may own; a pack that locates trailing markers itself **must** bound the
search by it, or a component followed by sibling content latches a *child's*
marker and duplicates server-rendered content after a bail.
- `server.afterRenderComponent` is append-only (the `html` argument is always
`''`), `server.assets` contributes modulepreload hints, and `getInjectedHTML` /
`getStreamingChunks` emit extra markup or streamed chunks.
> **You usually don't write this by hand.** The islands strategy — `client:*`
> directives, deferred hydration, signal-state transfer and per-island code
> splitting — is already implemented as a plugin in
> [`@sigx/ssr-islands`](/server/packages/ssr-islands/overview). Reach for the raw
> SPI only when building a *new* strategy.
## Next steps
- [Building a full SSR app](/server/packages/server-renderer/full-app)
- [Islands & selective hydration](/server/packages/ssr-islands/overview)
- [API reference](/server/packages/server-renderer/api)
---
url: https://sigx.dev/server/packages/server-renderer/rendering/
title: Rendering & streaming
description: Render a complete document with renderDocument into Express, Fastify, or an edge runtime, and load server data with useData/useStream
---
# Rendering & streaming
Hand the renderer an HTML template, let it own the whole response — head, shell, state, async content — and stream it into a real HTTP server.
## The document render APIs
The `renderDocument*` family takes a full HTML template containing an outlet
marker and assembles the complete document: collected `useHead()` tags injected
before ``, the app shell at the outlet, the serialized state blob, any
streamed async chunks, and the template tail. You no longer hand-splice
`template.replace('', html)` in your server.
| API | Returns | Default mode | Best for |
|---|---|---|---|
| `renderDocument` | `Promise
` | `'blocking'` | Buffer the full document, then send it once — crawlers, AI agents |
| `renderDocumentToNodeStream` | `{ stream: Readable; shell: Promise }` | `'stream'` | Node servers — Express, Fastify, H3 |
| `renderDocumentToWebStream` | `ReadableStream` | `'stream'` | Web-standard runtimes — Workers, Deno, edge |
All three accept a raw JSX element **or** an `App` from `defineApp()` (the `App`
form preserves `AppContext` for `inject()` and plugins such as a router), plus a
`DocumentOptions` object.
### The template and outlet
```tsx
const template = `
`;
```
The outlet marker defaults to ``; override it with the `outlet`
option. The renderer also splits the tail at ``: everything up to it —
including your entry `