Skip to content

Data & state

The Boundary

Every island is a serialization boundary. import.meta.og.$ is the one mark that carries anything across it, functions included, into context or props. Stores cross automatically. Refusals fail at your line, not in a serializer.

On this page

Each island is its own hydration root. Thus each value that travels from a server-rendered host into an island — a prop, a context value — crosses a serialization boundary. Plain data crosses with no work. All other values need a way to travel. The rule of ogygia for this is one sentence:

Mark the boundary with import.meta.og.$(). Everything that crosses is either data, or something that knows how to rebuild itself on the other side.

import.meta.og.$ — the one mark

og.$ wraps a value at the exact place where it goes to a boundary. What it does depends on what you give it:

A function — it crosses alive

<script>
	import { setContext } from 'ogygia';

	const tax = 0.19; // a local — captured by VALUE

	setContext('fmt', import.meta.og.$((n) => `€${(n * (1 + tax)).toFixed(2)}`));
</script>
<!-- inside any island, plain svelte: -->
<script>
	import { getContext } from 'svelte';
	const fmt = getContext('fmt'); // a REAL, callable function
</script>
<p>{fmt(100)}</p> <!-- €119.00 — synchronous, no eval in production -->

The compiler hoists the function. It changes each captured local into a bound value that crosses as data. The island binds it again. The function itself is never serialized. Its code ships as a build artifact. Its captures ship like any prop. Captures follow prop law: they must serialize. A captured store crosses live and stays shared — see below.

Two things are build errors, at your file and line:

  • a capture from a server-only module ($env/static/private, $app/server, *.server.ts) — the value would ship into client HTML;
  • a mark on something that can never cross (a DOM node, a bare function inside an object) — the error fires at the mark, not later inside a serializer.

Anything else — an explicit, checked crossing

<Cart
	items={import.meta.og.$(cartStore)}
	onCheckout={import.meta.og.$((id) => `/checkout/${id}`)}
/>

For a non-function value, og.$ is a boundary assertion. The value is classified at that line. A store passes, and will auto-wire. Plain data passes. A live DOM node or a class instance without a codec throws at that line, with the path inside the value that caused it. The value itself returns untouched. There is no wrapper to unlearn.

You never need og.$ for values that cross on their own (see the table below). But the mark is the recommended style. The next reader sees exactly where the page splits, and mistakes surface at the mark, not at serialize time.

What crosses, and how

You passHow it travelsNeeds a mark?
plain data, Map/Set/Dateserialized (devalue)no
a store (subscribe-shaped)value crosses; islands join to ONE live instance — set in island A repaints island Bno — automatic
a factory store (custom methods)value crosses; methods rebuilt from the factory on the other sideusually no — provable factories auto-brand; assert with og.store (below) in other cases
a class instancethrough its own codec: import.meta.og.wireone time, on the class
a functionhoisted code plus bound captures (og.$)yes — og.$
a snippetfreezes to HTML, or crosses live ({#snippet})no
a component / UIit is a region — a different noun by intentregion() / <Region>
a DOM node, a bare callback in an object, a secretrefused, with the path and a fix—

Stores: automatic, shared, live

<script>
	import { setContext } from 'ogygia';
	import { writable } from 'svelte/store';
	setContext('theme', writable('dark'));
</script>

Each island that reads getContext('theme') gets the same live store, not a copy. The current value crosses at render time. Identity is joined again client-side. Thus writes move between islands through plain Svelte reactivity. The same store provided under two keys stays one instance.

Factory stores

A store with custom methods needs its factory on the other side. Methods are code. They never serialize. Factories that the compiler can prove are branded automatically:

// auto-branded: the body provably returns a store
export const createCart = (seed = []) => {
	const { subscribe, set, update } = writable(seed);
	return { subscribe, set, update, add: (item) => update((x) => [...x, item]) };
};

For a factory it cannot prove (it returns the result of a helper), assert it:

export const createCart = import.meta.og.store((seed = []) => makeStore(seed));

The first parameter of the factory is the seed — the value that crosses. Sometimes a branded store reaches an island whose bundle never loaded the factory. It then degrades to a plain writable: the value stays, the methods are absent. A console warning names the one-import fix. It never crashes.

Two store shapes need care. A derived crosses as a frozen seed. The derivation cannot travel. Derive again client-side, or share the sources. Store values follow prop law: a callback inside the value of a store is still a bare function.

Keeping things home: { islands: false }

Not everything must cross. Some context is read only by the server-rendered host: a bag of element refs, a host-only callback, a very large config object. Keep it native with one option:

setContext('scrollRefs', refs, { islands: false });

Same-tree getContext operates as always. Nothing is serialized. In dev, ogygia warns when the island context payload grows past 32kB, and names the heaviest keys. This option is usually the fix.

Policy and errors

The boundary refuses keys that look like secrets (token, apiKey, …). Thus credentials do not ride into client HTML. Adjust it one time, server-side:

// hooks.server.ts
import { configure_boundary } from 'ogygia/internal';

configure_boundary({
	allow: ['tokenColor'],   // a false positive the sniff should let through
	deny: ['sessionBlob']    // a semantic secret the sniff can't see
});

Everything that the boundary declines is explained. A dropped context key logs the key, the dot-path to the leaf that caused it, and what to do instead. The design goal: nothing crosses, or fails to cross, silently.