Product · Schema & SDK

One Zod schema. A typed SDK, React hooks, and MCP tools.

Declare your models once in TypeScript with Zod. Ablo turns that one declaration into a fully typed ablo.<model> SDK, reactive React hooks, and MCP tools an AI assistant can call — no codegen, no drift.

Key takeaways

  • You declare your models once with Zod, in TypeScript — the schema is the single source of truth for the SDK, the hooks, and the MCP tools.
  • Every model becomes a typed ablo.<model> resource with retrieve, list, create, update, and delete — inputs and outputs inferred straight from the Zod shape, no codegen step to run.
  • The same schema powers reactive React hooks that re-render on live deltas, so reads stay in sync over WebSocket without manual subscriptions.
  • The same schema is auto-derived into MCP tools, so Claude Code, Cursor, and other MCP clients see real JSON-Schema arguments — they never invent fields or hallucinate IDs.
  • Adding a field is one edit to the Zod model plus a push; the SDK types, hook return types, and MCP tool schema all move with it.

Describe your data once

Most stacks make you describe your data three times: once for the database, once for the API client your app calls, and once again for whatever an AI assistant is allowed to do. Each copy drifts from the others, and every field you add is three edits in three files.

Ablo collapses that to one. You declare your models with Zod, push the schema, and the same declaration produces a typed SDK, reactive React hooks, and MCP tools. The contract your app codes against and the contract an agent calls through are generated from the same source, so they cannot disagree.

From one declaration to three surfaces

Start with a Zod model. The typed SDK, the reactive hooks, and the MCP tools below are all generated from it — change the model and all three move with it.

schema.ts
import { defineSchema, model } from '@abloatai/ablo';
import { z } from 'zod';

// One Zod-typed declaration is the source of truth for everything below.
export const schema = defineSchema({
  record: model({
    id: z.string(),
    title: z.string(),
    status: z.enum(['todo', 'doing', 'done']),
    assigneeId: z.string().nullable(),
    dueAt: z.string().datetime().nullable(),
  }),
});
server.ts — typed SDK
import { Ablo } from '@abloatai/ablo';
import { schema } from './schema';

const ablo = new Ablo({ schema, apiKey: process.env.ABLO_KEY });

// ablo.record is fully typed from the Zod model — no codegen step.
const { data } = await ablo.record.list({
  where: { status: 'doing' },          // typed against the enum
  orderBy: { dueAt: 'asc' },
});

await ablo.record.update({
  id: data[0].id,
  data: { status: 'done' },            // 'done' is checked; 'shipped' won't compile
});
RecordBoard.tsx — React hook
'use client';
import { useAblo, useSyncStatus } from '@abloatai/ablo/react';

export function RecordBoard() {
  // Reactive: re-renders when a delta arrives over WebSocket.
  const records = useAblo((ablo) =>
    ablo.record.local.list({ where: { status: 'doing' } }),
  );

  if (!useSyncStatus().isReady) return <Spinner />;
  return records.map((t) => <Card key={t.id} title={t.title} />);
}
record.update — MCP tool
// Auto-derived from the same Zod model — what an MCP client sees.
{
  name: 'record.update',
  description: 'Update a record record. Subject to claims + stale-context guards.',
  inputSchema: {
    type: 'object',
    required: ['id', 'patch', 'readAt'],
    properties: {
      id:     { type: 'string' },
      patch:  { type: 'object', properties: { status: { enum: ['todo', 'doing', 'done'] } } },
      readAt: { type: 'string', description: 'Stamp echoed from the prior retrieve.' }
    }
  }
}

What you get from the schema

Every model you declare turns into a typed resource your app, your components, and your agents all share.

Zod is the source of truth

Declare each model with defineSchema and model() using Zod fields. The schema is plain TypeScript — your editor type-checks and autocompletes it like any other code.

Typed ablo.<model> resources

Every model gets get, list, create, update, and delete. Arguments and return types are inferred from the Zod shape with z.infer — pass a status the enum doesn’t allow and it won’t compile.

Reactive React hooks

The same schema types useAblo selectors that read the local graph and subscribe to live deltas. Components re-render when the data changes over WebSocket — no manual subscription wiring.

Auto-derived MCP tools

Each model becomes an MCP tool with a real JSON-Schema. AI assistants get typed arguments and typed rejections, so they never invent fields or hallucinate IDs.

Name and object addressing

Address records by a stable name or object id, not only a raw UUID — so an agent can write “the Q3 plan” deterministically instead of guessing an identifier.

Evolve with a push

Add a field to the Zod model and push. The SDK types, hook return types, and MCP tool schema all move with it — one edit propagates end to end.

One schema, three consumers

The typed SDK is what your server and scripts code against — ablo.record.list, ablo.record.update — with inputs and outputs inferred from the Zod model. The React hooks are the same surface for the client, except reactive: they subscribe to the row’s deltas and re-render when the data changes. The MCP tools are the same surface for an AI assistant, exposed as JSON-Schema so the model gets typed arguments and typed errors. Three consumers, one declaration.

Coordination is built into the surface

Because the SDK, hooks, and MCP tools are generated together, the coordination primitives travel with them. A write tool carries the readAt stamp for stale-write rejection; a claim is a method on the same resource. An agent calling through MCP and your app calling through the SDK obey the same claims and the same lost-update guards — there is no second, weaker path around the rules.

Adopt your existing tables

You don’t have to start from a blank schema. Pull your existing tables into a schema, or check a declared schema against the live database to see the diff before you provision. The model layer adapts to your Postgres rather than asking you to migrate to a new database.

Frequently asked questions

Is there a code-generation step to run?

No. The SDK types are inferred directly from your Zod schema with z.infer, so ablo.<model> is fully typed the moment you import the schema — there is no generated client to regenerate and commit. You push the schema to provision tables, but the TypeScript types come from the declaration itself.

How do the React hooks stay in sync?

The hooks subscribe to the row’s sync group over WebSocket. When any successful write commits a delta, every subscribed client receives it and the hook re-renders with the new data — no polling and no manual subscription management.

What does an AI assistant see through MCP?

It sees one tool per model, each with a real JSON-Schema derived from the Zod fields — required keys, types, and enums. Because the arguments are typed, the assistant can’t invent a field your model doesn’t have, and write tools require the readAt stamp so a stale write is rejected rather than silently applied.

Can I use my existing Postgres tables?

Yes. You can pull existing tables into an Ablo schema and check a declared schema against the live database before provisioning. Ablo runs on top of your own Postgres rather than replacing it, so adoption is incremental.

What happens when I add a field?

You edit the Zod model and push. Because the SDK types, hook return types, and MCP tool schema are all derived from that one declaration, they all reflect the new field — there is no second or third place to update by hand.

Define it once, use it everywhere

A typed SDK, reactive hooks, and MCP tools from a single Zod schema — on top of your own Postgres.