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

# defineResource \[Tie a Drizzle table to an rxfy model]

`defineResource` — from `rxfy-server-drizzle` — ties a Drizzle `PgTable` to an rxfy
`ModelDescriptor`. You can either supply a pre-made model (the common case when sharing models
between server and client) or let the package derive one automatically via `drizzle-zod`.

:::note\[In-memory apps use `defineCollection`]
An app without Drizzle uses `defineCollection({ name, model, seed? })` from `rxfy-server-memory`
instead — it returns a resource whose binding is an in-memory `Map` (with `.all()` / `.get(id)`),
paired with `memoryStorage()` on [`createSync`](/framework/server/create-server).
:::

A resource pairs a `model` (from [`createModel`](/rxfy/create-model)) with a Drizzle `table` (a
`pgTable`). The tabs below show both sides:

:::code-group

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

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

export const resources = createResourceRegistry([userResource, postResource, commentResource]);
```

```ts [src/blog/models.ts]
import { createModel } from "rxfy";
import { z } from "zod";

// Branded ids keep post/user/comment ids from being mixed up — and are why the
// writers need a raw-row cast (see Writes).
const UserIdSchema = z.string().brand("UserId");
const PostIdSchema = z.string().brand("PostId");
const CommentIdSchema = z.string().brand("CommentId");

const UserSchema = z.object({ id: UserIdSchema, name: z.string(), email: z.string() });
const PostSchema = z.object({ id: PostIdSchema, userId: UserIdSchema, title: z.string(), body: z.string() });
const CommentSchema = z.object({ id: CommentIdSchema, postId: PostIdSchema, name: z.string(), body: z.string() });

export const userModel = createModel({ schema: UserSchema, getKey: (x) => x.id, name: "user" });
export const postModel = createModel({ schema: PostSchema, getKey: (x) => x.id, name: "post" });
export const commentModel = createModel({ schema: CommentSchema, getKey: (x) => x.id, name: "comment" });
```

```ts [src/db/schema.ts]
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull(),
});

export const posts = pgTable("posts", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull(),
  title: text("title").notNull(),
  body: text("body").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

export const comments = pgTable("comments", {
  id: text("id").primaryKey(),
  postId: text("post_id").notNull(),
  name: text("name").notNull(),
  body: text("body").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});
```

:::

**Name resolution** — `name` defaults to `model.name` when a model is supplied, then falls back to
the SQL table name. It becomes the topic namespace used for sync routing (`"posts:uuid-..."`) and
must match the rxfy model's own `name` so `patch` messages land in the right client store.

**`primaryKeyColumn`** — `defineResource` calls `primaryKeyColumn(table)` internally to locate the
single `primary: true` column. Composite primary keys are not supported in v1; the function throws
if it finds more than one PK column or a `primaryKeys`-style composite key.

## createResourceRegistry

`createResourceRegistry` — from core `rxfy-server` — indexes resources by name, rejects duplicates,
and exposes `byName`, `model`, and `all`. It is a neutral convenience lookup:
[`createSync`](/framework/server/create-server) does not require it, since the writers take a
resource directly.

`createResourceRegistry` lives in core `rxfy-server`; `defineResource` comes from
`rxfy-server-drizzle`, and `defineCollection` from `rxfy-server-memory`.
