ResourceKit

Introduction

What ResourceKit is, and why your app's data layer can feel local without replacing your backend.

ResourceKit is a full-stack data runtime for TypeScript apps. You describe your data once - issues, customers, files - keep your existing database and backend, let the engine execute on Postgres, REST, S3, the Stripe API, ClickHouse, or wherever your data is and get:

  • Reads that feel local. Queries answer from a local cache instantly and refresh in the background. Searching, sorting, and relation joins run on data that's already on the device - no spinner per keystroke.
  • Writes that feel instant. The UI updates the moment the user acts; the server confirms in the background. If the network is down, writes queue up and replay once you're back online - even across page reloads.
  • A server that stays in charge. On the server, your resources are backed by whatever actually holds the data - a Postgres table via Drizzle, plain server code, an external API - all behind the same typed interface.
  • Interconnect anywhere. Define relationships not just in your database but between any resource anywhere, e.g. between your local Workspace table and your Stripe instances.

Here's the whole loop - describe data, serve it, use it. The tabs are the three files you'd write; the preview is that exact app running live, right here in your browser (no server, persisted across reloads):

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",});
Preview

No tasks yet — add one.

How it fits into your stack

ResourceKit is not a database, an ORM, or a hosted backend. It sits between your UI and the backend you already have:

Your app Your backend /sync endpoint hooks access rules+ validation Components ResourceKit Client ResourceKit Server Postgres

On the client, the engine owns a local cache and answers queries from it immediately, refreshing from the server behind the scenes. On the server, your resources are backed by whatever actually holds the data - a Postgres table via Drizzle, plain server code, an external API - all behind the same typed interface.

The one idea worth knowing

Most of ResourceKit falls out of a single split:

  • The filter you pass to where() describes which records to sync. It's intentionally simple - equality, lists, ranges - because its only job is to move the right set of data to the device.
  • Everything you chain after it - .filter(), .orderBy(), .limit(), .include() - is plain TypeScript that runs locally against that synced set. It can be as rich as you like, because it never crosses the network.

That's why search-as-you-type costs zero requests, why narrowing a synced list is instant, and why ResourceKit never grows into a query language you have to learn: local querying is just TypeScript.

What it's good at

ResourceKit shines for app-like products: dashboards, project tools, admin panels, editors - anything where users work with a bounded set of data (their workspace, their account) and expect interactions to be immediate. It's incremental by design: start with one resource and a single endpoint, add actions, access rules, relations, offline persistence, and live updates only as you need them.

It deliberately does not run arbitrary client SQL on the server, replace your migrations or auth, or promise offline access to data that was never synced.

Where to go next

On this page