Globals on the page#

Open devtools on a sigx page and a handful of __SIGX_* names are there. This is what each one is, which of them you are meant to see, and the one thing worth knowing before you put a secret behind a useData key.

Every SSR framework puts state on a global, because the server has to hand the browser something and window is the channel — Next.js ships __NEXT_DATA__ and self.__next_f, Nuxt has __NUXT__, SvelteKit one hashed name, Solid _$HY. So the question is not whether to have them. It is which ones a person opening a console should ever see.

Two classes#

Wire seams are enumerable. They are the page's data cache and streaming bootstrap — the things you might legitimately want to inspect — and the server writes them with a plain assignment from an emitted <script>, so their descriptor is not ours to choose without changing what goes over the wire.

GlobalWhat it is
__SIGX_ASYNC__the page's data cache: key → value for everything useData and useAsync resolved, encoded by @sigx/serialize. Every mount of the same key restores from it, including after client-side navigation.
__SIGX_BOUNDARIES__id → SSRBoundaryRecord — per-boundary props and signal snapshots, for selective hydration and resume.
__SIGX_STREAMING_COMPLETE__true once the document has finished streaming. It is set alongside a sigx:ready event on window, and the event is the half most apps should use — the flag is for code that starts running after it already fired.
$SIGX_REPLACE(id, html) => void — swaps a resolved async boundary's placeholder for its HTML, then dispatches a bubbling sigx:async-ready event.
$SIGX_APPEND(id, text) => void — appends a streamed useStream text token into its placeholder.

Control seams are non-enumerable. Everything else. These are pack-to-pack wiring stamped by JavaScript at runtime — the server-app config, the serverFn codec and scope, the type handlers, the devtools hook. Nobody reads them off globalThis by hand, so they are kept out of Object.keys(globalThis) and console completion.

Two of them guard the install itself: __SIGX_REACTIVITY__ and __SIGX_RUNTIME_CORE__, each { version, url }, stamped when @sigx/reactivity and @sigx/runtime-core first evaluate. A second copy of either package that finds the stamp throws in development and warns once in production — see Stability & versioning. They are read only through readCopyStamp on @sigx/reactivity/internals.

Non-enumerable is not a security boundary. Anything running on the page can still read a hidden property by name. The point is surface area and legibility, not access control.

What the page exposes is yours, and unfiltered#

This is the part worth reading twice.

Whatever your fetcher returns lands verbatim in __SIGX_ASYNC__ under its key, and a claimed component's props and signals land in __SIGX_BOUNDARIES__[id]. Boundary records additionally carry canonical cache-key strings in deps, which can identify a user (user:42) even when the value itself does not.

The renderer applies key-safety and serializability checks, and no sensitivity filter. So:

TypeScript
// Everything this returns is readable from the console.
const account = useData('billing', () => getAccount(userId));

A secret fetched under a useData key is one window.__SIGX_ASYNC__ away. Fetch it in a server function the client calls when it needs it, or return only the fields the page actually renders.

Nothing the framework itself owns goes there. The principal lives on rq.locals under a non-enumerable Symbol.for slot that no serializer walks, and @sigx/server-renderer performs no globalThis writes at all.

Seeding a pack from the data cache#

A pack that owns client state — a store, an i18n catalog — can seed it from values the server transferred. The server writes them with ctx.registerSerializedState(key, value) (@sigx/server-renderer); the client reads them with peekRestored and, when it needs to, drops them with invalidateRestored, both from sigx / @sigx/runtime-core:

TypeScript
import { peekRestored, invalidateRestored } from 'sigx';

const seed = peekRestored('store:cart');       // { hit, value }
if (seed.hit) state.set(structuredClone(seed.value));
  • hit is own-key membership — a transferred null is a hit.
  • Reads do not consume. __SIGX_ASYNC__ is the page's data cache for its lifetime, so every later mount seeds from the same entry. For a seed that must not outlive one instance, peek and then call invalidateRestored(key).
  • Servers always miss — both accessors act only on a live client.
  • Copy first. The value is shared with the blob, not a private copy. Hand it to reactive state as-is and the pack's mutations write back into the blob — and from there into every later seed. structuredClone covers plain data and the built-in codec types; a value that can be a class instance needs a copy that knows the type.

Stamping a seam from your own pack#

Plain assignment works and stays hidden:

TypeScript
globalThis.__SIGX_MY_PACK__ = value;

An assignment to an existing writable data property inherits its descriptor, so a seam another pack already defined non-enumerably stays that way. To define a fresh one, match the convention:

TypeScript
Object.defineProperty(globalThis, '__SIGX_MY_PACK__', {
    value,
    writable: true,      // last-wins re-stamping
    configurable: true,  // so a dispose() path can delete it
});

A new property defined this way is non-enumerable by default, which is the behaviour you want — spell enumerable: false only when you may be overwriting a property that an older module copy created by assignment during HMR.

Reach for one global accessor module per seam rather than touching globalThis from several places; that is the rule the framework holds itself to, and it is why each of these has exactly one writer.

Next steps#

  • Data loading — what fills __SIGX_ASYNC__.
  • @sigx/server — the server-app control seam and why its config is frozen.
  • Hydration — how the boundary table is read.