A full-stack data runtime for TypeScript
Define your data once. Reads answer instantly from a local cache, writes apply optimistically and auto-sync between users, and the whole thing works offline - all on top of the database and server you already have.
import { z } from "zod";import { resource, action, engine } from "resourcekit";// ResourceKit bases its type safety on zod schemas of your dataconst taskSchema = z.object({ id: z.string().default(() => crypto.randomUUID()), workspaceId: z.string(), title: z.string().min(1), done: z.boolean().default(false), createdAt: z.string().default(() => new Date().toISOString()),});// Describe your data once - shared by client and server.export const tasks = resource("tasks", { schema: taskSchema, actions: { // Optionally, define actions to have nice client methods and consistent server logic. toggle: action(z.object({}), ({ record }) => ({ done: !record.done })), },});// The engine is the contract both sides build on.export const appEngine = engine({ resources: [tasks], endpoint: "/sync",});No tasks yet — add one.
The code on the left is the real, server-backed shape. The preview on the right is that same app running live in your browser.
A unified access layer to your data.
The client and server share a single, typed API to access your data. You declare your resources - the shape of your records, how to read and write them, and who can do what. The same API, the same access rules, the same types, everywhere.
Declare a resource once; on the server, point it at whatever holds its data. Same backbone slot, wildly different stores - the client speaks the same plans to all of them and never knows the difference.
backbone: drizzleBackbone(db, tasksTable),Relational rows, versioned for conflict detection.
backbone: mongoBackbone(db.collection("comments")),A document per record - no schema migration.
backbone: redisBackbone(redis),Instant lookups for small, hot datasets.
backbone: stripeCustomerBackbone(stripe),No database at all - and no .where(), enforced in the types.
And the client is identical for every one of them: const { data } = useSynced(tasks.where({ workspaceId })).
Everything an app's data layer should already do
The plumbing you'd otherwise hand-roll - caching, optimistic updates, offline queues, live updates - built in and typed end to end.
Instant reads
Queries answer from a local cache immediately and revalidate in the background. Filtering, sorting, and relation joins run on data that's already there.
Optimistic writes
The UI updates the moment a user acts; the server confirms behind the scenes. Rejections roll back automatically - no manual bookkeeping.
Offline-ready
Writes queue and replay in order when you reconnect. Opt into persistence and the cache - queued writes included - survives reloads.
Live across windows
One line streams server-pushed changes to every connected client over SSE. Bridge across instances with Redis when you scale out.
Your server stays in charge
Every request is validated against your schema and checked against access rules you declare once. The client is never trusted.
Keep your backend
Postgres via Drizzle, in-memory data, custom server code, external APIs - all behind one typed interface and a single sync endpoint.
Start with one resource.
Add it to a single screen, keep everything else as it is. Layer in actions, access rules, offline, and live updates only when you need them.
bun add resourcekit