# ADR 0004 — Universal state: serialization, hydration, scope, injection (v0.8)

- **Status:** Accepted — the hydration *surface* in decisions 1–2 (`dehydrate()`,
  `mount({ hydrate, queries })`, `<HydrationBoundary>`) is superseded by
  [ADR 0006](./0006-transparent-hydration.md); the payload shape, seeding semantics,
  and decisions 3–4 stand.
- **Date:** 2026-07-02
- **Context:** the v0.8 milestone — closing the "one mount everywhere" loop with SSR
  hydration, plus component-lifetime scopes and runtime injection. Builds on
  [ADR 0003](./0003-data-story.md).

## Context

"Write it once, run it anywhere" left a hole: server-rendered atom/query state was thrown
away, so hydration diverged from the client's module-initial values (a mismatch + a refetch).
v0.8 seeds the client from the server, and adds two smaller universal-state primitives.

## Decisions

### 1. Only schema-encoded values cross the wire, explicitly

`atom.serializable(initial, key, schema)` marks an atom as serializable — it carries a stable
key and an Effect `Schema`. `dehydrate()` snapshots those atoms' Schema-encoded values into a
plain payload; `mount(layer, Root, { hydrate })` (or `<HydrationBoundary>`) Schema-**decodes**
and seeds them before the first render. Server-resolved query results seed the same way via
`{ key, value, fetchedAt }` and `mount(..., { queries })`. Nothing else crosses: services,
layers, effects, and the runtime are never serialized. The client decode is the boundary
contract — a tampered payload fails decoding rather than corrupting state. This is the opposite
of `'use server'` RPC generation: the client/server split is purely the `react-server` export
condition, and no component code becomes a network endpoint. (Documented on the
[what-crosses-the-wire](../../landing/src/content/docs/what-crosses-the-wire.md) page.)

### 2. Hydration seeds *before the first render*

`HydrationBoundary` seeds atoms and queries in render (ref-guarded, once), synchronously, before
its children read them — so `getServerSnapshot`/the first read sees the seeded value and there is
no flash-of-initial-state. A server-seeded query entry starts with no effect and **adopts the
client query's effect** the first time it is read, so refetch/staleTime work afterward; its
staleness clock starts at the server fetch time.

### 3. Component-lifetime `Scope` via `scoped(effect)`

`scoped(effect)` runs an effect requiring a `Scope` against a scope tied to the component. The
`Scope` is a **child of the runtime's scope**, so disposing the runtime also closes it — the
finalizer runs whether the component unmounts (prompt, explicit close on the store entry's cancel)
or the whole runtime is torn down, with no race between the two. `Scope` is provided here, so it
does not bubble to `mount`. Acquisition is deduped per (component, position) and StrictMode-safe
via the existing claim/deferred-release lifecycle. On the server a `scoped` yield is wrapped with
`Effect.scoped` for the one-shot render.

### 4. `mount` accepts a pre-built `ManagedRuntime`

`mount(runtimeOrLayer, Root)` accepts a `ManagedRuntime` where a `Layer` is accepted (share one
runtime across mounts / with non-React code). effract **does not dispose an injected runtime** —
the caller owns its lifecycle. The compile-time missing-services check still applies, via the
runtime's `ROut`.

## Consequences

- SSR/RSC pages hydrate with no mismatch and no double-fetch — the payoff of the runtime-level
  query cache (ADR 0003) and serializable atoms.
- The store gained `seedQuery` + effect-adoption; the executor gained `withScope`.
- `@tmonier/create-effract` (a new package) scaffolds all of this and drops an agent-teaching AGENTS.md
  fragment (issue #53).

## Alternatives considered

- **Serialize everything automatically.** Rejected: a security and correctness hazard (the very
  thing `'use server'` critics warn about). Only explicit, schema-typed values cross.
- **A per-component scope owned outside the runtime.** Rejected: it races runtime disposal on
  unmount. A child of the runtime scope closes deterministically either way.
