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

OptionRequiredDescription
modulesYesimport.meta.glob("./convex/*.ts") — lazy module map pointing at your Convex functions. Must not use { eager: true }.
schemaNoDefault export from convex/schema.ts. Enables write-time validation.
nameNoIndexedDB database name. Tabs sharing the same name share data. Defaults to "convex-embedded".
syncNo{ url: string } — enables remote sync. Omit for a purely local embedded database.
authNoAuth 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, and omit.
  • Offline Sync — dive into the sync state machine and resolve flow.