<!--
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
-->

# Writes \[sync.create / sync.update / sync.delete and touch]

`sync.create`, `sync.update`, and `sync.delete` run the SQL write and then publish sync messages.
All three accept an optional `{ touch: [...] }` option to mark state channels stale.

`server/api.ts` pulls together three things declared elsewhere — the tabs below show each one:

* `live` — the `Live` object from [`createSync`](/framework/server/create-server).
* `postResource` — a resource from [`defineResource`](/framework/server/define-resource).
* `postsState` — a state from [`defineState`](/rxfy/define-state), the **same state you pass to
  `useStateData`** on the client. It already carries the `{ key, window }` a channel needs, so it
  doubles as a `StateChannelDescriptor`.

:::code-group

```ts [server/api.ts]
import { touch } from "rxfy-server";
import { postsState } from "../src/blog/states.js";
import { postResource } from "../src/blog/resources.js";
import { sync } from "./sync.js";

// update — publishes a `patch` on the entity topic automatically
await sync.update(postResource, postId, { title, body });

// create — does NOT publish a patch; touch the state channels that list this entity
await sync.create(postResource, { id: newId(), userId, title, body }, { touch: [touch(postsState, {})] });

// delete — same: no patch, touch the channels that referenced this entity
await sync.delete(postResource, postId, { touch: [touch(postsState, {})] });
```

```ts [server/live.ts]
import { createInMemoryHub, createSync } from "rxfy-server";
import { drizzleStorage } from "rxfy-server-drizzle";
import { db } from "./db.js";

export const hub = createInMemoryHub();

export const sync = createSync({ storage: drizzleStorage(db), hub });
```

```ts [src/blog/resources.ts]
import { postModel, userModel } from "./models.js";
import { defineResource } from "rxfy-server-drizzle";
import { posts, users } from "../db/schema.js";

export const userResource = defineResource({ table: users, model: userModel });
export const postResource = defineResource({ table: posts, model: postModel });
```

```ts [src/blog/states.ts]
import { array, defineState } from "rxfy";
import { z } from "zod";
import { postModel, userModel } from "./models.js";

// Declared once, imported by both the client (useStateData) and the server (touch).
export const postsState = defineState({
  key: "posts",
  params: z.object({}),
  model: { posts: array(postModel), authors: array(userModel) },
});
```

:::

**Publish behaviour:**

| Call                                    | SQL                | Publishes                                                                     |
| --------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
| `sync.update(resource, id, patch)`      | UPDATE … RETURNING | `patch` on the `"<name>:<id>"` entity topic, then `stale` on touched channels |
| `sync.create(resource, row, { touch })` | INSERT             | `stale` on touched channels only (no patch)                                   |
| `sync.delete(resource, id, { touch })`  | DELETE             | `stale` on touched channels only                                              |
| `sync.touch(...targets)`                | none               | `stale` out of band                                                           |

`create` and `delete` publish no entity patch — there is no row to push for a new entity, and the
deleted row is gone. They publish `stale` on every touched channel so clients know to refetch.

**Return values:** `create` resolves the inserted row. `update` resolves the updated row, or
`undefined` when no row matches the id — a not-found update writes nothing and publishes
nothing (no patch, no touch).

## touch

`touch(stateDescriptor, params)` builds a `TouchTarget` for a specific state instance. Window
dimensions declared in `state.window` (page, cursor, sort) are stripped from the channel key so
all windows of the same partition share one invalidation channel — see
[Grants](/framework/server/grants) for how channels are derived and signed.

You can also call `sync.touch(...targets)` directly without a write to invalidate channels out of
band.
