Quick Start
This guide walks through a complete setup: defining an embedded table, binding the server component, writing queries and mutations, and creating a client.
1. Define Your Schema
In convex/schema.ts, use embeddedTable() instead of defineTable() for tables you want to sync. Fields wrapped with schema.register() become CRDT-aware; plain v.* validators remain last-write-wins.
// convex/schema.ts
import { schema } from "@robelest/convex-embedded/crdt";
import { embeddedTable } from "@robelest/convex-embedded/server";
import { defineSchema } from "convex/server";
import { v } from "convex/values";
export const tasks = embeddedTable("tasks", {
title: schema.register(v.string()),
body: schema.register(v.string()),
});
export default defineSchema({
tasks,
}); The tasks handle returned by embeddedTable() is both a valid TableDefinition (so defineSchema accepts it) and a builder for mutations and queries.
2. Set Up Server Binding
Create convex/embedded.ts to bind the component reference. This enables CRDT delta recording on the remote Convex backend.
// convex/embedded.ts
import { setup } from "@robelest/convex-embedded/server";
import { components } from "./_generated/api";
setup({ component: components.embedded }); This file is side-effect-only. It binds components.embedded to every table registered with embeddedTable(). Function registration does not depend on this file — mutations and queries are registered at definition time.
3. Write Functions
Write your mutations and queries using the tasks handle from the schema. The API mirrors standard Convex functions:
// convex/tasks.ts
import { v } from "convex/values";
import { tasks } from "./schema";
export const resolve = tasks.resolve;
export const create = tasks.mutation({
args: { title: v.string(), body: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", args);
},
});
export const update = tasks.mutation({
args: { id: v.id("tasks"), title: v.string(), body: v.string() },
handler: async (ctx, args) => {
const { id, ...fields } = args;
await ctx.db.patch(id, fields);
return id;
},
});
export const remove = tasks.mutation({
args: { id: v.id("tasks") },
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
return args.id;
},
});
export const list = tasks.query({
args: {},
returns: v.array(
v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
title: v.string(),
body: v.string(),
}),
),
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
}); The resolve export is auto-generated by embeddedTable(). It handles CRDT catch-up queries on reconnect — you just need to re-export it.
4. Create the Client
In your app’s entry point, call createConvexClient(). It returns a standard ConvexClient that works with any Convex framework integration.
Key Options
| Option | Required | Description |
|---|---|---|
modules | Yes | import.meta.glob("./convex/*.ts") — lazy module map pointing at your Convex functions. Must not use { eager: true }. |
schema | No | Default export from convex/schema.ts. Enables write-time validation. |
name | No | IndexedDB database name. Tabs sharing the same name share data. Defaults to "convex-embedded". |
sync | No | { url: string } — enables remote sync. Omit for a purely local embedded database. |
auth | No | Auth configuration with fetchToken, getUserIdentity, and related hooks. |
5. Use Queries and Mutations
Once the client is provided to your framework, use queries and mutations exactly as you would with a normal Convex app:
The queries read from the local embedded database (instant, offline-capable). Mutations write locally first and replay to the remote backend in the background.
Next Steps
- Architecture — understand how the runtime, transport, and storage fit together.
- CRDT Fields — learn about
register,prose,counter,set, andomit. - Offline Sync — dive into the sync state machine and resolve flow.