<!--
Sitemap:
- [rxfy](/index): Typed, normalized, reactive state — built on RxJS
- [Comparison](/comparison): rxfy versus Redux Toolkit, MobX, Jotai, TanStack Query, and TanStack DB
- [Inspired by](/inspired-by): the libraries and ideas rxfy grew out of
- [Agent Skills](/agent-skills): Accurate rxfy context for AI coding assistants
- [Examples](/examples): Runnable apps, from client-only to fully synced
- [Changelog](/changelog)
- [Getting Started](/getting-started)
- [Create Store](/getting-started/create-store): Normalized reactive state in a client-only app
- [Add SSR](/getting-started/add-ssr): Render the first paint on the server, hydrate with no refetch
- [Add Sync Client](/getting-started/add-sync-client): The full stack: the server publishes, the client syncs
- [Core Concepts](/core-concepts): The ideas rxfy is built on
- [Observables](/core-concepts/observables): A value that changes over time, that you can subscribe to
- [Normalization](/core-concepts/normalization): Store each entity once, reference it by id
- [Late Unwrapping](/core-concepts/late-unwrapping): Unwrap async state at the leaf, not the trunk
- [Server-Side Rendering](/core-concepts/ssr): Dehydrate on the server, hydrate with no refetch
- [rxfy](/rxfy): The core package: atoms, lenses, models, and states
- [createModel](/rxfy/create-model): Typed entities in normalized storage
- [defineState](/rxfy/define-state): Typed, normalized state shapes with fetching and mutations
- [createAtom](/rxfy/create-atom): A reactive cell with synchronous get, set, and modify
- [createLens](/rxfy/create-lens): A two-way view into part of an Atom
- [React Bindings](/react): Hooks and helpers for using rxfy in React
- [useStateData](/react/use-state-data): Fetch, normalize, and subscribe to a query
- [useStatePagedData](/react/use-state-paged-data): Paginated and infinite-scroll lists
- [useModelStore](/react/use-model-store): Subscribe to one normalized entity by id
- [useAtom](/react/use-atom): Two-way binding for any IAtom
- [Pending](/react/pending): Render pending, rejected, and fulfilled UI for any observable
- [usePending](/react/use-pending): The status value behind Pending
- [useObservable](/react/use-observable): Bind a raw Observable to React
- [Sync Client in React](/react/sync-client): StoreProvider, useSyncClient, and update handles
- [rxfy-client](/framework/client): The framework-agnostic browser sync runtime
- [createSyncClient](/framework/client/create-sync-client): Connect a transport and drive the sync loop
- [readSsrGrants](/framework/client/read-ssr-grants): Lift SSR-embedded channel grants
- [rxfy-server](/framework/server): Bind Drizzle tables, write, and publish sync updates
- [defineResource](/framework/server/define-resource): Tie a Drizzle table to an rxfy model
- [createSync](/framework/server/create-server): Wire a storage adapter, hub, and secret into a Live object
- [createInMemoryHub](/framework/server/hub): The socket-keyed pub/sub backbone
- [Writes](/framework/server/writes): sync.create / sync.update / sync.delete and touch
- [Storage adapters](/framework/server/storage-adapters): Persist writes with Drizzle or in memory
- [Sync messages](/framework/server/messages): What travels between server and client
- [Grants](/framework/server/grants): The server signs what it serves; the client subscribes with the token
- [rxfy-ws](/framework/ws): The default WebSocket transport
- [createWsServer](/framework/ws/server): Attach a Hub to WebSocket connections
- [createWsClient](/framework/ws/client): The browser transport with reconnect and replay
- [Custom transports](/framework/ws/custom-transport): Bring your own ClientTransport
- [Guides](/guides): Task-focused walkthroughs of common rxfy patterns
- [Pagination and infinite scroll](/guides/pagination): Load and append pages into one normalized list
-->

# createModel \[Typed entities in normalized storage]

`createModel` defines a typed entity descriptor — the shape plus id-extraction logic — for
normalizing and sharing entities across state slices (see
[Normalization](/core-concepts/normalization)); `ModelStore` is the reactive store that holds
the live entities.

```ts
import { z } from "zod";
import { createModel } from "rxfy";

const TodoModel = createModel({
  schema: z.object({ id: z.string(), title: z.string(), done: z.boolean() }),
  getKey: (todo) => todo.id,
  name: "todo",
});
```

`name` is the model's stable string identity for SSR dehydration and sync topics —
symbols can't cross the server/client boundary, so the name is what addresses the
model's entities there. Keep it unique per app: registries warn on duplicates, since
two models sharing a name would mix their entities in `dehydrate` output.

## Low-level storage

`createModelRegistry` / `createModelStore` are the normalized storage primitives. In
React apps they are wired automatically by `StoreProvider`; use them directly for
non-React or custom setups.

```ts
import { asKey, createModelRegistry } from "rxfy";

const registry = createModelRegistry();
const users = registry.model(UserModel);

users.set("1", { id: "1", name: "Alice" });
// `get` takes a branded StoreKey, not a raw string — brand a hand-built id with `asKey`:
users.get(asKey(UserModel, "1")).get(); // { id: "1", name: "Alice" } — synchronous
users.get(asKey(UserModel, "1")).subscribe(console.log); // reactive: emits on every change
```

:::warning
`get()` on a key that has never had `set()` called **throws** — ids are expected to come
from fulfilled states, which normalize their entities into the store before handing out
ids. Use `getValue(key)` for a non-throwing probe.
:::

### StoreKey — get only accepts framework-minted ids

`get(key)` is typed to accept a **`StoreKey<T>`**, not a bare `string`. A `StoreKey` is a
phantom-branded id the framework mints: every id a state's `data$`/query shape hands you is
already one, so `store.get(post.author)` and `store.get(id)` (where `id` came from a query
shape) keep working with no change. A raw, hand-built string — a URL param, a literal — is
**not** a `StoreKey`, so `get("1")` is a type error. Brand it explicitly with `asKey`:

```ts
import { asKey } from "rxfy";

store.get(asKey(UserModel, routeParams.id)); // the one sanctioned way to enter the keyspace
```

This makes "you tried to read an entity from an id you never fetched" a compile-time error
instead of a runtime throw. `set`, `setMany`, and `getValue` still take plain `string` keys.

A `ModelStore<T>` exposes:

```ts
type ModelStore<T> = {
  get<TView extends T = T>(key: StoreKey<TView>): IAtom<TView>; // writable handle; the key's brand picks the view; throws if not loaded
  set(key: string, val: T): void; // write one entity
  setMany(items: T[]): void; // write many; key is derived via getKey
  getValue(key: string): T | undefined; // synchronous read, no subscription, never throws
  observe(key: string): Observable<T | undefined>; // non-throwing reactive read; undefined until present
  valueEntries(): [string, T][]; // snapshot of all loaded [key, value] pairs
  added$: Observable<string>; // a key, the first time its entity appears
};
```

Inside React, `useModelStore(descriptor)` returns this same store from the provider's
registry; `useModelRegistry()` returns the underlying registry (and throws if no
`StoreProvider` is mounted).

### Observing what enters the store

`added$` emits a key the **first time** an entity becomes present (the first `set`); later
updates to that key don't re-emit. New subscribers **replay** the keys already in the store, so
a late subscriber still learns about everything loaded so far, exactly once, with no gap.

```ts
import { createModelRegistry } from "rxfy";
import { UserModel } from "./models";

const users = createModelRegistry().model(UserModel);

users.added$.subscribe((key) => console.log("now live on", key));
users.set("1", { id: "1", name: "Alice" }); // → "now live on 1"
users.set("1", { id: "1", name: "Bob" }); // (update, no emit)
```

The registry exposes the same signal across every store, tagged with the model name:

```ts
import { createModelRegistry } from "rxfy";
import { UserModel } from "./models";

const registry = createModelRegistry();

registry.added$.subscribe(({ name, key }) => console.log(`${name}:${key}`));
registry.model(UserModel).set("1", { id: "1", name: "Alice" }); // → "user:1"
```

It replays entities already in the registry and follows stores created later. This is what
lets a sync layer track exactly what the client holds without each query wiring its
ids in by hand; see [rxfy-server](/framework/server).

## ModelStore get

`get(key)` returns the entity's cell itself — a writable `IAtom<T>` for rendering, field
[Lenses](/rxfy/create-lens), and form inputs. Repeated calls return the same handle (stable
identity, no wrapper allocation). Reads reflect the store; `.set(next)` writes back
(equivalent to `store.set(key, next)`), so the change propagates to every subscriber of
that entity.

```ts
import { createModelRegistry } from "rxfy";
import { UserModel } from "./models";

const users = createModelRegistry().model(UserModel);
users.set("1", { id: "1", name: "Alice" });

const user$ = users.get(asKey(UserModel, "1")); // IAtom<User>
user$.get(); // { id: "1", name: "Alice" }
user$.set({ id: "1", name: "Bob" }); // === users.set("1", { id: "1", name: "Bob" })
```

It assumes the entity is already loaded and **throws** for a key that has never been
`set` — an id in hand should always have come from a fulfilled state, so an unloaded
access is a programming error surfaced early, not a loading state. For the React
form-binding pattern, see [`useAtom` & two-way binding](/react/use-atom).

## Relations between models

A model field can reference another model. Declare it in the schema with `ref` (to-one) or
`refArray` (to-many); the referenced entity normalizes into its own store, and the field holds
its `StoreKey`:

```ts
import { z } from "zod";
import { createModel, ref } from "rxfy";

const CategoryModel = createModel({
  schema: z.object({ id: z.string(), name: z.string() }),
  getKey: (c) => c.id,
  name: "category",
});

const PostModel = createModel({
  schema: z.object({
    id: z.string(),
    title: z.string(),
    categoryId: z.string(), // plain FK column — always present
    category: ref(CategoryModel), // the relation — resolvable when a state joins it
  }),
  getKey: (p) => p.id,
  name: "post",
  fk: { category: "categoryId" }, // links the relation to its FK column (both sides inferred from the schema)
});
```

A **state** decides per-fetch whether to deliver the relation joined or as a bare id —
see [`.with()` on defineState](/rxfy/define-state#relations--per-state-joins). The
optional `fk` map is type-safe: its keys autocomplete to the model's relation fields and its
values to the schema's plain columns. It records which FK column a relation mirrors, so a live
sync `patch` keeps the relation resolvable.

Once a state joins a relation, reads of it are [view-typed](/react/use-model-store#view-typed-reads) —
the joined field is required, no `!` — and you can [infer the id/payload types](/rxfy/define-state#inferring-a-states-types)
(`NormalizedOf`, `ViewOf`, …) rather than hand-write them.
