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.
| Global | What 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:
// 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:
import { peekRestored, invalidateRestored } from 'sigx';
const seed = peekRestored('store:cart'); // { hit, value }
if (seed.hit) state.set(structuredClone(seed.value));
hitis own-key membership — a transferrednullis 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 callinvalidateRestored(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.
structuredClonecovers 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:
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:
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.
