Prometheus Entity Management API
    Preparing search index...

    Module @prometheus-ags/prometheus-entity-management

    @prometheus-ags/prometheus-entity-management

    Normalized, globally-reactive entity graph store for React

    Update a post in one screen and every list row, detail panel, and badge that reads that entity updates automatically—without hand-maintained query keys. Normalization is built around your type + id + normalize function, not a separate cache product. The same graph holds data from REST, GraphQL, WebSocket / Supabase / Convex, Prisma-shaped APIs, and ElectricSQL + PGlite local-first sync.

    The 3.2.0 package line and its React 19/Vite 8 source-workspace showcase is implemented. The production-browser gate covers normalized cross-view updates, optimistic confirm/rollback, relationships, view completeness modes, REST/GraphQL seams, realtime coalescing, PGlite/Loro, Suspense/error containment, DevTools, and accessibility.

    See the React 19/Vite 8 release guide for the assembled example evidence. Install the matching stable core and React pair with:

    pnpm add @prometheus-ags/entity-graph-core@3.2.0 \
    @prometheus-ags/prometheus-entity-management@3.2.0 \
    react@^19 react-dom@^19

    Release boundary: npm 3.2.0 includes the optional ./devtools and development-only ./devtools/auto inspector entries documented below. The ordinary package root remains inspector-free.

    Doc Purpose
    ../../docs/tanstack-query-and-table.md How this library fits with TanStack Query and TanStack Table
    ../../docs/tanstack-comparison.md Detailed comparison against TanStack DB, Query, Table, AI, and Intent
    ../../docs/advanced.md Engine, GC, Suspense, DevTools, SSR, testing
    ../../RELEASING.md Versioning, prepublishOnly, npm publish
    CHANGELOG.md Release history
    ../../release/binding-singleton-contract.md Core peer ownership and packed singleton verification
    ../../release/vite-react19-example.md React 19/Vite 8 showcase scenarios, commands, and evidence boundary
    ../../prometheus-entity-skills/_shared/references/devtools-react-inspector.md Optional React inspector entries, activation, privacy, and workflow contract

    pnpm add @prometheus-ags/entity-graph-core @prometheus-ags/prometheus-entity-management react react-dom
    

    (npm install works for consumers too; this repository itself is pnpm-only.)

    @prometheus-ags/entity-graph-core is a required peer: install it explicitly so the application owns the one compatible graph instance. The React package must not install a private core copy. See the binding singleton contract.

    type Post = { id: string; title: string; status: string };
    
    import { useEntity } from "@prometheus-ags/prometheus-entity-management";

    export function PostCard({ postId }: { postId: string }) {
    const { data, isLoading, error } = useEntity<Post, Post>({
    type: "Post",
    id: postId,
    fetch: async (id) => {
    const res = await fetch(`/api/posts/${id}`);
    if (!res.ok) throw new Error(String(res.status));
    return res.json() as Post;
    },
    normalize: (raw) => raw,
    });

    if (isLoading) return <p>Loading…</p>;
    if (error) return <p>{error}</p>;
    if (!data) return null;
    return <article>{data.title}</article>;
    }

    Any other component that calls useEntity with the same type and id reads the same normalized record from the graph.


    Each (entityType, id) maps to a single canonical object in the Zustand graph (entities[type][id]). Lists and detail views never keep their own full copies; they resolve through that node.

    useEntity, useEntityList, and GraphQL hooks describe how to load data and how to normalize it into the graph. They do not own isolated cache entries the way query-key–scoped caches do.

    List state keeps ordered IDs plus pagination metadata. Rows are items joined from entities at render time, so when Post:123 changes, every list that includes that ID re-renders consistently.

    Data flows up into the graph; UI reads down through hooks (see Architecture).

    ┌─────────────────────────────────────────────────────────────┐
    │ Layer 3: UI Components (optional, users can build their own)│
    │ src/ui/ │
    │ EntityTable · EntityDetailSheet · EntityFormSheet · columns │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 2: Access Patterns (hooks - how components read data) │
    │ src/hooks.ts, src/graphql/hooks.ts, src/crud/ │
    │ useEntity · useEntityList · useEntityView · useEntityCRUD │
    │ useGQLEntity · useEntityMutation · useEntityAugment │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 1: Entity Graph (Zustand store - canonical data) │
    │ src/graph.ts │
    │ entities[type][id] · patches[type][id] · lists[queryKey] │
    └─────────────────────────────────────────────────────────────┘
    ▲ ▲ ▲ ▲
    REST fetch GraphQL WebSocket ElectricSQL

    Eight focused additions across three patch releases for tenant-scoped, PGlite-backed local-first apps. All backward compatible; no new runtime dependencies.

    API File Purpose
    createPGlitePersistenceAdapter(pglite, options?) src/adapters/pglite-persistence.ts GraphPersistenceAdapter that stores the snapshot in a PGlite table (_graph_snapshot by default)
    createTenantScopedElectricAdapter(opts) src/adapters/electricsql-tenant.ts Refuses to attach Electric shapes that lack a tenantColumn; builds the WHERE from a validated { companyId } claim so shape predicates can never widen past RLS
    registerEntityFromSql({ entityType, createTableSql, overrides }) src/schema-from-sql.ts Generates and registers a JSON Schema directly from a Postgres CREATE TABLE block — no hand-maintained TypeScript schema duplicates
    useEntityListAsTable(opts) src/table/use-entity-list-as-table.ts Wraps useEntityList for TanStack Table — returns a referentially-stable data array and rowCount; no TanStack Table dep required
    startLocalFirstGraph({ ..., retryPolicy }) src/local-first-runtime.ts Retry-with-backoff for pending offline action replay; exhausted actions go to an opt-in poisonHandler instead of looping forever
    • useEntityList return shape is now useMemo-stabilised. React 19's useSyncExternalStore was detecting a fresh object on every render and emitting an infinite-loop warning. Identity-stable items + stable pagination state means no more perpetual loading skeletons from hook composition chains.
    • isError: boolean added to both UseEntityListResult and UseEntityViewResult as a convenience alias for error !== null, matching TanStack Query's hook ergonomics.
    • setListError now stamps lastFetched and clears stale, closing a terminal-error retry loop where a 404 on a missing table triggered an infinite refetch storm.
    • useEntityView writes errors to the base key (the one isLoading reads from) and defaults isLoading to false when no list state exists, so a failed first fetch no longer leaves consumers stuck in a perpetual loading state.

    The graph runtime now exposes a focused set of non-hook helpers for loaders, workflows, and orchestration:

    • queryOnce(...) / selectGraph(...) for one-shot graph snapshots without a live subscription
    • nested include projections over normalized graph data
    • createGraphTransaction(...) / createGraphAction(...) for explicit optimistic graph writes with rollback
    • sync-aware snapshot metadata: $synced, $origin, $updatedAt
    • createGraphEffect(...) for enter/update/exit reactions over graph query results
    • createGraphTool(...) / exportGraphSnapshot(...) for AI interoperability without bundling an AI runtime
    • startLocalFirstGraph(...), hydrateGraphFromStorage(...), and persistGraphToStorage(...) for PWA/PGlite-friendly graph persistence and replay
    • schema-driven entity rendering via registerEntityJsonSchema(...), buildEntityFieldsFromSchema(...), and useSchemaEntityFields(...)
    • built-in markdown-aware schema fields plus schema-aware graph export helpers for A2UI/runtime-generated entity models

    These additions are intentionally graph-native. They extend the entity graph for orchestration use cases without changing the core Components → Hooks → Stores → APIs/Realtime architecture.


    Feature @prometheus-ags/prometheus-entity-management TanStack Query Apollo Client SWR
    Normalized cache Yes (automatic) No (manual) Yes (manual config) No
    Cross-view reactivity Yes No Partial No
    REST support Yes Yes No Yes
    GraphQL support Yes No (separate client) Yes No
    Realtime / WebSocket Yes (built-in adapters) No (manual) Yes (subscriptions) No
    Local-first (ElectricSQL) Yes No No No
    Prisma integration Yes No No No
    CRUD lifecycle Yes (useEntityCRUD) No No No
    Relation schemas Yes (cascade invalidation) No Yes (type policies) No
    Suspense hooks Yes Yes Yes Yes
    SSR hydration Yes Yes Yes Yes
    Garbage collection Yes (automatic, configurable) Yes Yes No
    Bundle size See Bundle size ~39KB ~130KB ~4KB

    Peer dependencies (react and react-dom) are not included in any column. The package installs @tanstack/react-table because the root entry exports the built-in table components that use it at runtime. Published dist sizes change with each release—measure before quoting numbers in docs or talks.

    The npm package ships a single large entry (dist/index.mjs) that re-exports the full surface (hooks, GraphQL, CRUD, view, UI, adapters). Your app’s gzipped cost depends on tree-shaking, minification, and which imports you use.

    Maintainers: after pnpm run build, a rough gzip size of the ESM bundle is:

    gzip -c dist/index.mjs | wc -c
    

    Compare against peers only when measurement methodology matches (minified vs unminified, gzip vs brotli, ESM vs CJS).


    Export Description
    createGraphStore / graphStore Create an isolated vanilla graph or access the default core singleton.
    GraphStoreProvider Scope all descendant React hooks to an application-owned GraphStore; use one request graph per SSR render and one hydrated graph per mounted browser tree.
    useGraphStoreApi Resolve the nearest provider-owned graph, falling back to the public singleton. React-hook ownership reference-counts focus/reconnect listeners and GC, releasing them after the final hook unmounts.
    useGraphStore React hook subscribed to the nearest scoped graph. Deprecated compatibility getState, setState, getInitialState, and subscribe delegates remain singleton-only throughout 3.x and emit one development diagnostic per method. Prefer domain hooks in components.
    configureEngine App-wide defaults: stale time, retries, GC interval, GC time, etc.
    getEngineOptions Read merged engine options.
    serializeKey Stable string key for list queryKey serialization.
    fetchEntity Imperative single-entity fetch with dedupe and graph write (for custom hooks/adapters).
    fetchList Imperative list fetch with dedupe and graph write.
    dedupe Process-global in-flight promise deduplication helper.
    startGarbageCollector(storeApi?) / stopGarbageCollector(storeApi?) Periodic eviction of unsubscribed, stale entities in the selected graph; omitting the store targets the compatibility singleton.
    attachGlobalListeners(storeApi?) Reference-count focus/reconnect listeners and GC for one graph; call the returned disposer to release the attachment. React hooks manage this automatically.

    useGraphStore's imperative surface (getState / setState / subscribe / getInitialState) resolves the active graph on every call, so store actions, module-level helpers, and mutation callbacks honour a mounted GraphStoreProvider without being rewritten as hooks. Resolution order is:

    1. the request scope opened by runWithGraphStore(store, fn),
    2. the module-level store published by a mounted GraphStoreProvider,
    3. the package singleton.

    With no provider and no request scope this is exactly the pre-3.0.5 behaviour, so existing imperative callers are unaffected.

    Server code must open a request scope. GraphStoreProvider is React context and cannot scope Server Components or module-level functions, and a module-level store would leak across concurrent requests. Wrap each request:

    import { createGraphStore, runWithGraphStore, prepareGraphStoreScope } from "@prometheus-ags/entity-graph-core";

    // Once at startup — REQUIRED under pure ESM, where there is no synchronous
    // `require` to load node:async_hooks. Without it request scoping degrades to
    // the module-level store and warns.
    await prepareGraphStoreScope();

    // Per request
    runWithGraphStore(createGraphStore(), () => renderThisRequest());

    Capturing useGraphStoreApi() during render is still the most explicit option inside React, and injecting a GraphStore parameter remains available for code that wants no ambient resolution at all.

    Export Description
    queryOnce / selectGraph One-shot graph snapshot queries with local filtering, sorting, and nested includes.
    createGraphTransaction Explicit graph write transaction with commit / rollback.
    createGraphAction Higher-level optimistic action wrapper around graph transactions.
    createGraphEffect Subscribe to graph query results with onEnter, onUpdate, and onExit.
    createGraphTool Typed graph-backed helper for AI or workflow integrations.
    createSchemaGraphTool Schema-aware graph tool helper for AI workflows built around dynamic entity schemas.
    exportGraphSnapshot Serialize graph data for prompts, exports, and non-React workflows.
    Export Description
    registerEntityJsonSchema / registerRuntimeSchema Register static or runtime-generated JSON Schemas for an entity type or JSON column.
    registerEntityFromSql Generate and register a JSON Schema from a Postgres CREATE TABLE block — eliminates hand-maintained TypeScript schema duplicates.
    getEntityJsonSchema Resolve the active schema by entity type, schema id, or field.
    buildEntityFieldsFromSchema Generate entity field descriptors from JSON Schema for dynamic forms and detail views.
    useSchemaEntityFields Hook that resolves a registered schema and returns generated field descriptors.
    MarkdownFieldRenderer / MarkdownFieldEditor Built-in markdown-aware schema field renderer and editor.
    exportGraphSnapshotWithSchemas Serialize graph data together with resolved entity schemas.
    Export Description
    startLocalFirstGraph Starts a higher-level local-first runtime for graph hydration, persistence, action replay, and sync status. Accepts optional retryPolicy for offline action replay.
    hydrateGraphFromStorage Restore graph state from a storage adapter using a JSON-serializable snapshot payload.
    persistGraphToStorage Persist graph state and pending action metadata through a storage adapter.
    useGraphSyncStatus Hook exposing online/offline/hydrating/syncing/ready state for PWAs and IPC-safe hosts.
    getGraphSyncStatus / graphSyncStatusStore Imperative reader and vanilla store for non-component local-first orchestration.
    replayActionWithRetry Replay a single pending action with configurable exponential-backoff retry.
    createPGlitePersistenceAdapter PGlite-backed GraphPersistenceAdapter; stores the snapshot in a PGlite table alongside synced data.
    Export Description
    useEntity Subscribe to one entity; fetch/normalize into graph; SWR + subscriber-aware refetch. Returns { data, isLoading, isError, error, refetch }.
    useEntityList Subscribe to a list query key; stores IDs; merges row data from graph. Returns { items, isLoading, isError, error, isFetching, fetchNextPage, refetch }.
    useEntityView Filter/sort/search with local/remote/hybrid completeness modes. Returns { items, isLoading, isError, error, setFilter, setSort, setSearch }.
    useEntityMutation Mutate with optional optimistic updates and list invalidation hooks.
    useEntityAugment Patch UI-only fields merged at read time across all subscribers.
    useEntityListAsTable Wraps useEntityList with a referentially-stable data array + rowCount for TanStack Table.
    useSuspenseEntity Suspense variant of useEntity (non-null id required).
    useSuspenseEntityList Suspense variant of useEntityList.
    Export Description
    FilterSpec, SortSpec Transport-agnostic filter and sort AST types.
    toRestParams Compile view → REST query params.
    toSQLClauses Compile view → SQL-style WHERE / ORDER BY fragments.
    toGraphQLVariables Compile view → common GraphQL variable shapes.
    toPrismaWhere / toPrismaOrderBy Compile view → Prisma-style where / orderBy objects.
    applyView, compareEntities, matchesFilter, matchesSearch, checkCompleteness Local evaluation and completeness helpers.
    flattenClauses, hasCustomPredicates Filter introspection utilities.
    Export Description
    useEntityCRUD Unified list + detail + edit + create flow, edit buffer, dirty tracking, optimistic helpers.
    registerSchema Register entity relations for cascade invalidation after mutations.
    getSchema, readRelations, cascadeInvalidation Introspection and imperative cascade invalidation.
    Export Description
    RealtimeManager Registers adapters, coalesces changes (16 ms window), writes to graph.
    getRealtimeManager, resetRealtimeManager Singleton access and test resets.
    createWebSocketAdapter Generic WebSocket → graph changes.
    createSupabaseRealtimeAdapter Supabase Realtime payloads → graph.
    createConvexAdapter Convex-shaped streams → graph.
    createGraphQLSubscriptionAdapter GraphQL over WebSocket subscriptions → graph.
    createFlintAdapter Flint watchEntities stream → graph through the structural client contract.
    publishFlintMutation Publish one entity record through the caller-owned Flint client.
    Export Description
    createGQLClient Configure endpoint, fetcher, and entity descriptors for normalization.
    GQLClient Client class instance type.
    normalizeGQLResponse / executeGQL Normalize and execute with the same descriptor model.
    useGQLEntity, useGQLList Graph-backed entity and list hooks.
    useGQLMutation, useGQLSubscription GraphQL mutation and subscription hooks tied to the graph.
    Export Description
    createPrismaEntityConfig Factory for REST endpoints that speak Prisma-style where / orderBy query params.
    prismaRelationsToSchema Convert Prisma-style relation map → EntitySchema for registerSchema.
    toPrismaInclude Build an include map from relation descriptors.
    Export Description
    createElectricAdapter ElectricSQL / PGlite shape changes → graph.
    createTenantScopedElectricAdapter Safety wrapper: refuses to attach a shape unless it declares a tenantColumn; builds the WHERE clause from a validated { companyId } claim.
    useLocalFirst Hook for local-first workflows with the adapter.
    usePGliteQuery Run queries against PGlite in sync with the graph story.

    The ordinary package root keeps the lightweight legacy useGraphDevTools hook and does not import or mount the inspector. The full inspector ships in 3.2.0 behind two optional subpaths:

    Entry/export Description
    ./devtools/auto Side-effectful debug opt-in that detects development mode, waits for the browser, and mounts the launcher.
    EntityGraphDevtools from ./devtools SSR-safe explicit host for provider-owned or Next.js graphs.
    EntityGraphDevtoolsProvider and inspector hooks from ./devtools Advanced multi-store, custom host, and state-adapter integration.

    For Vite, add one debug-only dynamic import near the client entry:

    if (import.meta.env.DEV) {
    void import("@prometheus-ags/prometheus-entity-management/devtools/auto");
    }

    For Next.js, mount the explicit host from a client component after hydration and pass the same provider-owned graph used by the application:

    "use client";

    import { useEffect, useState } from "react";
    import type { GraphStore } from "@prometheus-ags/entity-graph-core";

    type Host = typeof import(
    "@prometheus-ags/prometheus-entity-management/devtools"
    )["EntityGraphDevtools"];

    export function DevelopmentGraphTools({ store }: { store: GraphStore }) {
    const [Host, setHost] = useState<Host | null>(null);
    useEffect(() => {
    if (process.env.NODE_ENV === "production") return;
    let active = true;
    void import("@prometheus-ags/prometheus-entity-management/devtools")
    .then((module) => {
    if (active) setHost(() => module.EntityGraphDevtools);
    });
    return () => {
    active = false;
    };
    }, []);
    return Host ? <Host mode="auto" store={store} /> : null;
    }

    After opt-in, development mode shows a floating Graph launcher. Open it to inspect Overview, Entities, Views, Activity, Graph Pulse causality, canonical originals, local patches, merged live values, registered rendered-view membership, retained entity history, and controller-owned time travel. The display menu can move or compact the launcher, dock the inspector, hide it until reload, or hide it for the browser. Restore/toggle it with Ctrl/Cmd+Shift+G.

    Serialized/remote inspection remains metadata-only unless the host explicitly enables an include/redact value policy. The same-origin embedded inspector can read the selected local store without sending values over a transport. The optional Loro adapter is loaded only when a consumer uses that integration.

    Export Description
    EntityTable, InlineCellEditor Table + inline cell editing wired to the graph / view layer.
    EntityDetailSheet, EntityFormSheet, Sheet CRUD-oriented sheet primitives.
    selectionColumn, textColumn, numberColumn, dateColumn, enumColumn, booleanColumn, actionsColumn, SortHeader Column helpers with filter metadata for tooling.

    GraphState, EntityState, ListState, EntityType, EntityId, EntitySyncMetadata, EntitySnapshot, EngineOptions, EntityQueryOptions, ListQueryOptions, ViewDescriptor, EntitySchema, RelationDescriptor, realtime adapter types, GraphQL types, CRUD types, and column meta types are all exported from the package entry.


    Before (TanStack Query)

    const { data, isLoading } = useQuery({
    queryKey: ["post", id],
    queryFn: () => fetch(`/api/posts/${id}`).then((r) => r.json()),
    });

    After (entity graph)

    const { data, isLoading } = useEntity<Post, Post>({
    type: "Post",
    id,
    fetch: (postId) => fetch(`/api/posts/${postId}`).then((r) => r.json()),
    normalize: (raw) => raw,
    });

    Difference: the graph key is (type, id), not an opaque query key. Anything else that uses the same type/id shares that record—no setQueryData across keys.

    Before

    const { data } = useQuery({
    queryKey: ["posts", { status }],
    queryFn: () => api.posts.list({ status }),
    });

    After

    const { items, isLoading } = useEntityList<Post, Post>({
    type: "Post",
    queryKey: ["posts", { status }],
    fetch: (p) => api.posts.list({ status, page: p.page, pageSize: p.pageSize, cursor: p.cursor }),
    normalize: (row) => ({ id: row.id, data: row }),
    });

    Difference: the list stores IDs; row objects are always read through the normalized Post map, so updates propagate everywhere.

    If you wire useEntityList directly into TanStack Table's data prop, the table treats a new array reference as new data on every render. Use useEntityListAsTable instead—it returns a referentially-stable data array that only changes when the underlying items actually change.

    Before

    const { data = [] } = useQuery<Post[]>({
    queryKey: ["posts"],
    queryFn: () => api.posts.list(),
    });
    const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });

    After

    import { useEntityListAsTable } from "@prometheus-ags/prometheus-entity-management";

    const { data, rowCount, isLoading, isError, error } = useEntityListAsTable<Post, Post>({
    type: "Post",
    fetch: (p) => api.posts.list(p),
    normalize: (row) => ({ id: row.id, data: row }),
    });

    const table = useReactTable({ data, rowCount, columns, getCoreRowModel: getCoreRowModel() });

    Difference: data identity is stable across renders where items haven't changed, so TanStack Table's memoization works correctly and row state (selection, expansion) is preserved between refetches.

    Before

    const qc = useQueryClient();
    const mutation = useMutation({
    mutationFn: (id: string) => api.posts.archive(id),
    onSuccess: () => {
    qc.invalidateQueries({ queryKey: ["posts"] });
    qc.invalidateQueries({ queryKey: ["post"] });
    },
    });

    After

    import { serializeKey, useEntityMutation } from "@prometheus-ags/prometheus-entity-management";

    const { mutate } = useEntityMutation<string, Post, Post>({
    type: "Post",
    mutate: (id) => api.posts.archive(id),
    normalize: (raw) => ({ id: raw.id, data: raw }),
    optimistic: (id) => ({ id, patch: { status: "archived" } }),
    invalidateLists: [serializeKey(["posts"])],
    });

    Difference: optimistic updates target the entity; optional list invalidation is declarative. Cross-view consistency comes from normalization, not from remembering every query key.


    Before (Apollo)

    const { data } = useQuery(GET_POST, { variables: { id } });
    

    After

    const { data } = useGQLEntity({
    client: gqlClient,
    document: GET_POST,
    variables: {},
    type: "Post",
    id,
    descriptor: postDescriptor,
    });

    Use useGQLList with document, queryKey, descriptor, and getItems to map the response into rows.

    Difference: you describe entities with descriptors (how to normalize IDs and nested types) once; you do not maintain a parallel universe of type policies and merge functions for every edge case.

    Before

    const [mutate] = useMutation(UPDATE_POST);
    

    After

    const { mutate } = useGQLMutation({
    client: gqlClient,
    document: UPDATE_POST,
    type: "Post",
    descriptors: [postDescriptor],
    });

    Descriptors tell the client how to write normalized entities from the mutation payload—no Apollo-style type policies.

    Before

    useSubscription(POST_UPDATED, { variables: { id } });
    

    After

    useGQLSubscription({
    client: gqlClient,
    wsClient: gqlWsClient,
    document: POST_UPDATED_SUB,
    variables: { id },
    descriptors: [postDescriptor],
    });

    Difference: GraphQL, REST, and realtime adapters can all write the same entity graph, so mixed stacks do not need two caches.


    createPrismaEntityConfig targets REST APIs that accept Prisma-style where and orderBy as JSON query parameters (typical for Prisma-backed route handlers).

    import {
    createPrismaEntityConfig,
    registerSchema,
    useEntity,
    useEntityList,
    } from "@prometheus-ags/prometheus-entity-management";

    type Post = { id: string; title: string; authorId: string };

    const Posts = createPrismaEntityConfig<Post>({
    type: "Post",
    endpoint: "/api/posts",
    relations: {
    author: { type: "User", foreignKey: "authorId", relation: "belongsTo" },
    comments: { type: "Comment", foreignKey: "postId", relation: "hasMany" },
    },
    });

    // Register cascade rules once (e.g. app init)
    Posts.schemas().forEach(registerSchema);
    function PostDetail({ postId }: { postId: string }) {
    const { data } = useEntity(Posts.entity(postId));
    return data ? <h1>{data.title}</h1> : null;
    }

    function PostList() {
    const { items } = useEntityList(
    Posts.list({
    filter: [{ field: "status", op: "eq", value: "published" }],
    sort: [{ field: "createdAt", direction: "desc" }],
    })
    );
    return (
    <ul>
    {items.map((p) => (
    <li key={p.id}>{p.title}</li>
    ))}
    </ul>
    );
    }

    Use Posts.crud() with useEntityCRUD when you want the full list + detail + forms pipeline against the same endpoints.


    Example Path What it demonstrates
    Vite app examples/vite-app/ Full CRUD, realtime adapters, TanStack Query → graph bridge (/tanstack-bridge), EntityTable / sheets, mock API with latency
    Next.js app examples/nextjs-app/ Next.js 16 App Router with a new createGraphStore() per document request, serializable RSC snapshot, one hydrated GraphStoreProvider, zero duplicate client fetches, route persistence, validated Server Action mutation, and client-only realtime takeover.

    From the repo root (this monorepo uses pnpm):

    pnpm install
    pnpm run dev:vite # http://localhost:5173
    pnpm run dev:next # http://localhost:3000

    • Components → Hooks → Stores → APIs / realtime — UI uses hooks only; hooks orchestrate; network and adapters update the graph.
    • Up into the graph: fetches, mutations, and realtime events call into the Zustand store.
    • Down from hooks: useEntity, useEntityList, useEntityView, GraphQL hooks, and CRUD read merged entities + patches.
    1. entities — Canonical server-shaped records per (type, id).
    2. patches — Local-only overlays (_selected, _loading, …) merged at read time.
    3. lists — Ordered ids[], pagination, and fetch flags — not duplicated row payloads.
    4. syncMetadata — Per-entity sync/provenance state layered into snapshot reads as $synced, $origin, and $updatedAt.

    In-flight deduplication, retries, subscriber ref-counting, stale-while-revalidate, optional periodic garbage collection for entities without subscribers.

    Adapters emit a shared change shape; the manager batches updates per animation frame to avoid UI thrash.

    One FilterSpec / SortSpec can compile to REST, SQL, GraphQL variables, or Prisma shapes, and can run locally when the graph already holds enough data.

    useEntityCRUD keeps the edit buffer in React state so other views stay on committed data until save; registerSchema drives relation-aware cascade invalidation.


    pnpm install

    # Examples
    pnpm run dev:vite
    pnpm run dev:next

    # Typecheck
    pnpm run typecheck
    pnpm run typecheck:vite
    pnpm run typecheck:next

    # Production builds of examples
    pnpm run build:vite
    pnpm run build:next

    # Clean artifacts
    pnpm run clean

    The library is consumed from source via path aliases in examples during development (no separate build step required for local hacking).


    MIT © Prometheus AGS / KnowMe LLC

    devtools
    devtools/auto