AbsoluteJS

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).

TS
// 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:

TS
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:

TS
import { treaty } from '@elysiajs/eden';
import type { App } from '../server'; // type-onlyno 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:

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:

TS
// 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.