Instructions for AI agents working on this codebase.
Resync is a framework for building realtime universal (SSR + client) Reason React applications with PostgreSQL-backed state. The monorepo contains reusable packages and demo apps.
# Install dependencies
pnpm install
# Build everything (continuous watch)
pnpm run dune:watch
# Run ecommerce demo (requires separate terminal for each)
pnpm run ecommerce:watch # restarts server on UI rebuild
# Run todo demo
pnpm run todo:watch # restarts server on UI rebuild
# Build a specific target
dune build @app # ecommerce client (from demos/ecommerce)
dune build demos/ecommerce/server/src/server.exe # ecommerce server (from repo root)
dune build demos/todo/ui/src/.build_stamp # todo client (from repo root)
dune build demos/todo/server/src/server.exe # todo server (from repo root)For AI agents: Due to orchestration tool issues with running dune directly (stdout/stderr capture, timeout handling), agents should prefer using the Python wrapper:
python scripts/run_dune.py build @all-apps
python scripts/run_dune.py build packages/universal-reason-react/store/js
python scripts/run_dune.py exec ./packages/universal-reason-react/store/test/store_runtime_test.exe
python scripts/run_dune.py cleanThe wrapper runs dune from the repo root, captures output cleanly, and has a configurable timeout (default 120s, override with DUNE_TIMEOUT env var). End users should run dune directly.
After making changes, always rebuild to verify:
dune build @appfromdemos/ecommerce(or equivalent for the target you changed)dune build demos/<demo>/server/src/server.exefrom repo root
Each demo requires specific env vars in .envrc:
- Ecommerce:
DB_URL,API_BASE_URL,ECOMMERCE_DOC_ROOT - Todo:
TODO_DOC_ROOT(set to./_build/default/demos/todo/ui/src/) - Todo Multiplayer:
DB_URL,TODO_MP_DOC_ROOT(set to./_build/default/demos/todo-multiplayer/ui/src/)
Ecommerce demo uses Docker for PostgreSQL:
docker compose up -d # starts postgres with auto-initialized schema + triggersUse Playwright MCP for end-to-end browser testing. Do NOT create Node.js test scripts.
Browser test scripts compile with Melange and run with Node.js:
# Video chat demo browser tests
pnpm run video-chat:test:browserThe lsp_diagnostics tool has limited support for Reason (.re) and OCaml (.ml) files in this project, especially in test directories. It commonly returns:
No supported source files found in directorywhen scanning directories containing.reor.mlfilesNo diagnostics foundfor individual files even when build errors exist
Workaround: rely on dune build (via the Python wrapper) as the source of truth for compile errors and type issues, rather than LSP diagnostics.
packages/
realtime-schema/ # SQL annotation parser, codegen, PPX, CLI
src/
Realtime_schema_types.ml
Realtime_schema_parser.ml
Realtime_schema_codegen.ml
Realtime_schema_ppx.ml
Realtime_schema_caqti.ml
universal-reason-react/
router/ # Universal router (shared route tree for server + client)
store/ # Tilia-backed offline-first store with SSR, persistence, realtime sync
js/ # JS/Melange implementations
StoreBuilder.re # Public runtime store builders
StoreCrud.re # Generic CRUD helpers for realtime patches
StoreIndexedDB.re # IndexedDB confirmed-state and action-ledger storage
StoreOffline.re # Local and synced runtime implementations
StorePatch.re # Patch decoding infrastructure
StoreSource.re # Tilia source wrappers
native/ # Server-side copies
reason-realtime/
pgnotify-adapter/ # PostgreSQL LISTEN/NOTIFY adapter
dream-middleware/ # Dream websocket middleware
esbuild-plugin/ # Client component extraction
universal-reason-react/ # Also: components, lucide-icons, intl
demos/
ecommerce/ # Full demo: DB, realtime, SSR
server/sql/ # Annotated SQL files (source of truth)
generated/ # Auto-generated triggers, migrations, snapshots
server/src/ # Dream server
ui/src/ # Client UI (Reason React)
shared/js/ # Shared types (Model.re, RealtimeSchema.ml)
shared/native/ # Server-side shared types
todo/ # Minimal demo: SSR, hydration, no DB/realtime
This project uses server-reason-react to render the same ReasonReact code on both the server (native OCaml) and the client (JavaScript via Melange). This is not an isomorphic Node.js setup — the server is a compiled native binary.
- Melange compiles
.refiles to.jsin a(melange.emit ...)target directory (e.g.,app/) - esbuild bundles the Melange JS output into
Index.re.jsandIndex.re.cssfor the browser - The server compiles the same
.refiles as native OCaml viaserver-reason-react's PPX transforms - The server's dune file copies
.resources fromui/src/into the server build context using(copy_files) - Shared types live in
shared/js/(Melange) andshared/native/(native), each with their own dune library
Code that runs on both server and client must compile under both targets. This means:
- Use
Js.Array.*for array operations — these are polyfilled by server-reason-react for native. Do NOT useArray.append(OCaml stdlib) orList.*for store collections. - Use
->chaining forJs.Array.*methods — they use[@mel.this]binding:items->Js.Array.filter(~f=...) - Use
switch%platformfor platform-specific behavior (DOM access, localStorage, etc.) - Use
[@platform js]/[@platform native]for platform-specific module implementations (provided by server-reason-react PPX) - Do not use browser-only APIs outside of
[@platform js]blocks (e.g.,document,window,fetch) - Do not use Node.js APIs or npm packages — the server is native OCaml, not Node
Each package that needs to work on both targets has two dune libraries:
shared/
js/dune # (library ...) with melange PPX preprocessors
native/dune # (library ...) with native PPX preprocessors
The server depends on *_native libraries; the client depends on *_js libraries. Both compile the same .re source files.
- Reason (
*.re) for application code and store/router libraries - OCaml (
*.ml) for infrastructure (PPX, codegen, CLI, native adapters) - SQL (
*.sql) with comment annotations for schema definitions
All stores use the terminal value-level builders (StoreBuilder.buildLocal, StoreBuilder.buildSynced, or StoreBuilder.buildCrud) with the pipeline API. Do NOT define top-level bindings and then pass them through as let x = x:
// CORRECT: terminal builder with inline pipeline
module StoreDef =
(val StoreBuilder.buildLocal(
StoreBuilder.make()
|> StoreBuilder.withSchema({
storeName: "todo.simple",
emptyState: { /* ... */ },
reduce: (~state, ~action) => state,
makeStore: (~state, ~derive=?, ()) => { /* ... */ },
})
|> StoreBuilder.withJson(~state_of_json, ~state_to_json, ~action_of_json, ~action_to_json)
|> StoreBuilder.withLocalPersistence(
~storeName="todo.simple",
~scopeKeyOfState=_state => "default",
~timestampOfState=state => state.updated_at,
~stateElementId=None,
(),
)
));
include (
StoreDef:
StoreBuilder.Runtime.Exports
with type state := state
and type action := action
and type t := store
);
type t = store;
module Context = StoreDef.Context;
// WRONG: top-level bindings passed to functor
let emptyState = { /* ... */ };
let reduce = (~state, ~action) => state;
module Runtime = StoreBuilder.Runtime.Make({
let emptyState = emptyState;
let reduce = reduce;
});Runtime behavior:
StoreBuilder.buildLocalis local-only and persists confirmed snapshots to IndexedDB, then propagates newer confirmed state across tabs withBroadcastChannelStoreBuilder.buildSyncedandStoreBuilder.buildCrudpersist confirmed snapshots plus an IndexedDB action ledger, send typed JSON actions over websocket, and propagate optimistic actions plus confirmed snapshots across tabs- For synced stores, cross-tab updates should not depend solely on websocket delivery; optimistic replay comes from the shared IndexedDB ledger
Store state uses array throughout. Use Js.Array.* functions (which work in both Melange JS and native via server-reason-react):
Js.Array.filter(~f=..., items)— notList.filterJs.Array.map(~f=..., items)— notList.mapJs.Array.concat(~other=..., items)— notArray.appendorList.append- Use
->chaining forJs.Array.*methods (they use[@mel.this]):items->Js.Array.filter(~f=...) Array.lengthis fine for getting length
Use StoreCrud for standard CRUD tables. Do NOT write custom patch types, upsert/delete functions, or manual StorePatch.Pg.decodeAs wiring:
type patch = StoreCrud.patch(MyItem.t);
// Inside functor body:
let decodePatch =
StorePatch.compose([
StoreCrud.decodePatch(
~table=RealtimeSchema.table_name("items"),
~decodeRow=MyItem.of_json,
(),
),
]);
let updateOfPatch = StoreCrud.updateOfPatch(
~getId=(item: MyItem.t) => item.id,
~getItems=(config: config) => config.items,
~setItems=(config: config, items) => {...config, items},
);For multi-table stores, compose decoders and use a wrapped variant:
type patch =
| ItemsPatch(StoreCrud.patch(Item.t))
| UsersPatch(StoreCrud.patch(User.t));Annotated SQL files in server/sql/ are the source of truth. The PPX reads them at compile time.
Key annotations:
-- @table <name>— marks a table for realtime-- @id_column <col>— primary key column-- @broadcast_channel column=<col>— which column determines the NOTIFY channel-- @broadcast_parent table=<parent> query=<named_query>— child table triggers parent re-broadcast-- @composite_key <col1>, <col2>— composite primary key-- @query <name>— named query in block comments (/* ... */)-- @json_column <col>— column returned as::textthat needs JSON normalization in triggers
Agents must NEVER run git checkout, git revert, or any command that discards user work without explicit user approval. This includes:
git checkout -- <file>to revert changesgit reset --hardto discard work- Any automatic "cleanup" that removes user-modified files
If you believe changes should be reverted, present the case to the user and wait for their explicit approval before proceeding.
Especially package.json, watch scripts, or unrelated config files.
REALTIME_QUERY_REFACTOR.md— full architecture plan for the realtime schema systemdocs/universal-reason-react.store.md— store API reference and patternsdocs/dream-router-store-setup.md— step-by-step guide for Dream + Router + Store setupdocs/API_REFERENCE.md— complete API reference for all packagesdocs/reason-realtime.pgnotify-adapter.md— PostgreSQL adapter docsdocs/reason-realtime.dream-middleware.md— Dream websocket middleware docs