---
title: What crosses the wire
description: In a framework where you never type 'server' or 'client', only two things cross the network — schema-encoded serializable atoms and server-resolved query results — both explicit, both validated by a Schema; no code becomes an implicit RPC endpoint.
group: Advanced
order: 3
---

effract lets you write one component and one `mount` that render in a SPA, during SSR, or as a React
Server Component, and you **never type `'use server'` or a client/server annotation** to get there. That
raises a fair, pointed question: if the boundary is invisible, what actually goes over the network, and
is it safe?

The short answer: effract's client/server split is **not** RPC magic. It comes entirely from the
package's [export-condition split](/docs/client-and-server/) — the `react-server` condition vs the
`default` — deciding which *implementation of `mount`* your bundler picks. **Nothing in your component
code implicitly becomes a network endpoint.** Only two kinds of *value* cross the wire, both explicit and
opt-in, and both carry a schema.

## Only two things cross — and you opt into each

### 1. Dehydrated serializable atoms

An ordinary `atom` **never** crosses the wire. Only an atom you deliberately make serializable does.
`atom.serializable(key, initial, schema)` adds a stable identity `key` — key first, because the key *is*
the atom's label — and an Effect `Schema` for encode/decode:

```tsx
import { atom } from '@tmonier/effract';
import * as Schema from 'effect/Schema';

// A cell that can cross the wire — identity key + schema contract.
const theme = atom.serializable('app/theme', 'light' as 'light' | 'dark', Schema.Literals(['light', 'dark']));
```

The transfer itself is automatic. On the server, `renderToStream` (from the server-only
`@tmonier/effract/ssr` subpath) renders the tree, waits for it to settle, and embeds every serializable
atom's **Schema-encoded** value as one JSON `<script>` after the HTML:

```tsx
import { renderToStream } from '@tmonier/effract/ssr';

const stream = await renderToStream(AppLive, Root, {
  shell: (app) => <Document>{app}</Document>,
  bootstrapModules: ['/client.js'],
});
return new Response(stream, { headers: { 'content-type': 'text/html' } });
```

On the client there is nothing to write: the ordinary `mount(AppLive, Root)` finds that script and seeds
the atoms **before the first render reads them**. The `atom.serializable` declaration is the only opt-in
— the rest of the round-trip has no user-facing API.

### 2. Server-resolved query results

A [`query`](/docs/loading/) the server already awaited rides along automatically: `renderToStream`
collects every **keyed, settled** query as `{ key, value, fetchedAt }` into the same embedded payload,
and the client `mount` seeds its query cache with them — so the matching `query(effect, key)` renders
without suspending or re-fetching. `fetchedAt` is when the server fetched; the client's staleness clock
starts there rather than resetting on arrival. Keyless `suspend`s (no identity to match) and pending or
failed queries (nothing worth shipping) are skipped — the client fetches those itself.

## What is validated, and where

The schema **is** the boundary contract. When a serializable atom is seeded, the client runs
`Schema.decodeUnknownSync(schema)` on the incoming value before it ever reaches your state. A tampered or
malformed payload **fails decoding** — it throws at the boundary rather than silently corrupting an
atom's type. You get validation on entry for free, because you already declared the schema to make the
atom serializable at all.

Seeded **query** values are different, and worth stating honestly: they are the plain values your own
server render resolved, embedded and seeded as-is — effract does not impose a schema on them. If a
query's *source* is untrusted, decode inside the query's effect (`Schema.decodeUnknownSync` on the fetched
payload) — then both the server render and any client fetch validate the same way.

## What never crosses the wire

Services, layers, `ManagedRuntime`, and effects are **never** serialized or sent. A REC body, a
service's `make`, a mutation's `run` effect — all execute where they run; only *values* (encoded atoms
and query results) traverse the network. There is no automatic RPC: effract does not read your source
and generate server endpoints from it.

## How this differs from `'use server'` RPC generation

With `'use server'`, a function marked in your source becomes a **callable network endpoint** the client
can invoke — the framework generates an RPC surface, and with it an attack surface, out of
ordinary-looking functions. effract does none of that. The client/server boundary is purely which
`mount` implementation the bundler picks via the `react-server` export condition; the only thing that
ever traverses the network is the state of atoms you explicitly marked serializable, plus the results of
queries the server already resolved. Nothing you write implicitly becomes network-callable.

This is closer in spirit to TanStack Start's deliberate rejection of implicit `'use server'` magic: the
boundary is **data + schema**, not generated RPC.

## The security posture, as a checklist

- **Only schema-encoded values cross.** A serializable atom carries a `Schema`; an ordinary `atom` never
  leaves the process.
- **Decode validates on entry.** The client runs `Schema.decodeUnknownSync` on each hydrated atom — a
  bad payload throws instead of corrupting state.
- **Make untrusted data go through a schema.** Seeded query values are whatever your server render
  resolved; decode untrusted sources inside the query's effect so server and client validate alike.
- **Secrets stay in services.** Services never serialize, so a secret held in a service never crosses the
  wire — **don't put a secret in a serializable atom.**
