End-to-end types
Sync's typed surface rides Elysia's Eden + TypeBox stack — no codegen step. Routes you declare with hydrateRoute / mutateRoute become the typed entry points; treaty<typeof app> gives a fully-typed client that syncStore consumes for optimism + reconnect + offline. Row and result types flow end-to-end from the route signatures.
#The layering — Eden owns the typing, sync owns the cache
The guiding decision behind the typed sync surface: don't build a parallel type system — lean all the way into Eden + TypeBox. Eden already solves typed transport + validation; the sync engine only owns what Eden can't: the stateful client (local cache, diffs, optimistic writes, offline).
// Concern | Owner
// ──────────────────────────────────────── | ──────────────────────────────
// Types over the wire + runtime validation | Eden + TypeBox (`t`)
// One-shot read (hydrate) + mutate | Elysia routes (Eden-typed)
// Live { added, removed, changed } diffs | syncSocket (WebSocket)
// Local reactive cache / optimism / offline | syncStore (client)#Server — ordinary Elysia routes
Hydrate and mutate are normal Elysia routes. The hydrateRoute / mutateRoute helpers turn a typed collection or mutation definition into a route handler whose return type carries the row or result type, so treaty<typeof app>() infers it on the client. TypeBox (t, re-exported by Elysia) validates and types the query / body:
import { Elysia, t } from 'elysia';
import {
createSyncEngine,
defineMutation,
hydrateRoute,
mutateRoute
} from '@absolutejs/sync/engine';
import { syncSocket } from '@absolutejs/sync';
import { prismaCollection } from '@absolutejs/sync/prisma';
const engine = createSyncEngine();
const orders = prismaCollection({
name: 'orders',
where: (p: { userId: number }) => ({ userId: p.userId, status: 'open' }),
find: (where) => prisma.order.findMany({ where }),
authorize: (p, ctx: { userId: number }) => p.userId === ctx.userId
});
engine.register(orders);
const createOrder = defineMutation({
name: 'createOrder',
handler: async (
args: { total: number },
ctx: { userId: number },
actions
) => {
const order = await prisma.order.create({
data: { ...args, userId: ctx.userId }
});
await actions.change('orders', { op: 'insert', row: order });
return order;
}
});
engine.registerMutation(createOrder);
const auth = (c: { userId?: number }) => ({ userId: c.userId ?? 0 });
const app = new Elysia()
.use(syncSocket({ engine, resolveContext: auth })) // live diffs (WS)
.get('/sync/orders', hydrateRoute(engine, orders, auth), {
query: t.Object({ userId: t.Numeric() })
})
.post('/sync/createOrder', mutateRoute(engine, createOrder, auth), {
body: t.Object({ total: t.Number() })
});
// The Eden export — type-only, no server code shipped to the client.
export type App = typeof app;Why explicit routes, not a builder: TypeScript can't infer route types from a runtime loop over definitions. The only way to get Eden types without reimplementing Elysia's route-chaining generics (fragile, version-coupled) is real chained routes. They're also a feature — per-route guards, rate limits, and derive all work because they are Elysia routes.
#Client — literally Eden + a generic store
syncStore is generic: the row type is inferred from hydrate's return, mutate args + result from mutate's signature. No <T>, no parallel schema, no custom inference:
import { treaty } from '@elysiajs/eden';
import type { App } from '../server'; // type-only — no server code shipped
import { syncStore } from '@absolutejs/sync/client';
const api = treaty<App>('localhost:3000');
const orders = syncStore({
// Eden-typed: row type inferred from the GET return
hydrate: () => api.sync.orders.get({ query: { userId } }),
// Eden-typed: args + result inferred from the POST signature
mutate: (a) => api.sync.createOrder.post(a),
// Live diffs over the WS — the diff frame's row shape matches `hydrate`
diffs: { collection: 'orders', params: { userId } }
});
orders.subscribe((s) => render(s.data)); // s.data: Order[] — types via Eden
await orders.mutate(
{ total: 42 },
{ optimistic: (d) => d.set({ id: tmp, status: 'open' }) }
);#OpenAPI / Scalar UI is automatic
@absolutejs/absolute auto-mounts @elysiajs/openapi in dev by default. Every Elysia route you declared — including the sync hydrate and mutate routes — shows up at /openapi with the Scalar UI, with its TypeBox schemas surfaced as parameters and response types. Visit http://localhost:3000/openapi and explore it without any extra wiring. Opt in for production via absolute.config.ts:
// absolute.config.ts — openapi is on by default in dev. Customize for prod:
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
openapi: {
documentation: {
title: 'My app',
description: 'Live API + sync surfaces'
},
provider: 'scalar' // or 'swagger'
}
});
// In dev: visit http://localhost:3000/openapi for the Scalar UI.
// Every Elysia route you declared shows up — including the sync hydrate /
// mutate routes — with their TypeBox schemas, parameters, and response
// types. No extra wiring; @absolutejs/absolute mounts @elysiajs/openapi
// for you.#Optimism, offline, reconnect — what the store does for you
All the stateful client work — optimistic overlays, roll-back-on-reject, drop-overlay-on-WS-diff, offline queueing, reconnect-with-catch-up-diff via the version cursor — lives in syncStore. The types come from Eden; the runtime semantics come from the store:
// The runtime model under `syncStore`:
//
// - Confirmed state comes from the WS (`syncSocket`): a snapshot on
// subscribe, then ordered diffs.
// - Mutations go over Eden HTTP (typed). `syncStore` applies an
// optimistic overlay, awaits the call, then reconciles:
// - on reject, rolls back the overlay
// - on resolve, drops the overlay once the WS diff has reflected
// the touched keys (with a short grace fallback)
// - Offline: pending `mutate` calls are queued and replayed on
// reconnect; optional durable storage survives reload.
// - Version cursor: snapshots + diffs carry a monotonic version; on
// reconnect the client resumes from `since`, the engine replays
// a catch-up diff (falls back to snapshot if the log can't cover
// the gap).
//
// No flickers, no double-applies, no manual reconciliation. All types
// flow from `typeof app` through Eden.If useSyncCollection from the Framework Hooks section is the batteries-included quickstart, syncStore + treaty<typeof app> is the path for projects that want end-to-end types and control over how Eden is wired. Both rest on the same engine and WebSocket protocol.