Skip to content
GitHubXDiscordRSS

LiveStore (Cloudflare)

LiveStore lets you build local-first, real-time, collaborative apps by combining an event-sourced SQLite state with automatic syncing.

This guide walks you through setting up a LiveStore sync backend on Cloudflare Workers with D1 and connecting it to a React client with Alchemy.

  1. Install dependencies

    Terminal window
    bun add alchemy @livestore/livestore @livestore/adapter-web @livestore/react @livestore/sync-cf react react-dom vite
  2. Create folder structure

    Create src/livestore/ and add the following files.

  3. Tablessrc/livestore/tables.ts

    Define your SQLite tables and any client documents used for local-only UI state.

    import { Schema, SessionIdSymbol, State } from "@livestore/livestore";
    // You can model your state as SQLite tables (https://docs.livestore.dev/reference/state/sqlite-schema)
    export const tables = {
    todos: State.SQLite.table({
    name: "todos",
    columns: {
    id: State.SQLite.text({ primaryKey: true }),
    text: State.SQLite.text({ default: "" }),
    completed: State.SQLite.boolean({ default: false }),
    deletedAt: State.SQLite.integer({
    nullable: true,
    schema: Schema.DateFromNumber,
    }),
    },
    }),
    // Client documents can be used for local-only state (e.g. form inputs)
    uiState: State.SQLite.clientDocument({
    name: "uiState",
    schema: Schema.Struct({
    newTodoText: Schema.String,
    filter: Schema.Literal("all", "active", "completed"),
    }),
    default: {
    id: SessionIdSymbol,
    value: {
    newTodoText: "",
    filter: "all",
    },
    },
    }),
    };
  4. Eventssrc/livestore/events.ts

    Describe all domain events (and reference tables.uiState.set for client docs).

    import { Events, Schema } from "@livestore/livestore";
    import { tables } from "./tables.ts";
    // Events describe data changes (https://docs.livestore.dev/reference/events)
    export const events = {
    todoCreated: Events.synced({
    name: "v1.TodoCreated",
    schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
    }),
    todoCompleted: Events.synced({
    name: "v1.TodoCompleted",
    schema: Schema.Struct({ id: Schema.String }),
    }),
    todoUncompleted: Events.synced({
    name: "v1.TodoUncompleted",
    schema: Schema.Struct({ id: Schema.String }),
    }),
    todoDeleted: Events.synced({
    name: "v1.TodoDeleted",
    schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
    }),
    todoClearedCompleted: Events.synced({
    name: "v1.TodoClearedCompleted",
    schema: Schema.Struct({ deletedAt: Schema.Date }),
    }),
    uiStateSet: tables.uiState.set,
    };
  5. Materializerssrc/livestore/materializers.ts

    Map each event to mutations on your SQLite tables.

    import { State } from "@livestore/livestore";
    import { events } from "./events.ts";
    import { tables } from "./tables.ts";
    // Materializers are used to map events to state (https://docs.livestore.dev/reference/state/materializers)
    export const materializers = State.SQLite.materializers(events, {
    "v1.TodoCreated": ({ id, text }) =>
    tables.todos.insert({ id, text, completed: false }),
    "v1.TodoCompleted": ({ id }) =>
    tables.todos.update({ completed: true }).where({ id }),
    "v1.TodoUncompleted": ({ id }) =>
    tables.todos.update({ completed: false }).where({ id }),
    "v1.TodoDeleted": ({ id, deletedAt }) =>
    tables.todos.update({ deletedAt }).where({ id }),
    "v1.TodoClearedCompleted": ({ deletedAt }) =>
    tables.todos.update({ deletedAt }).where({ completed: true }),
    });
  6. Queriessrc/livestore/queries.ts

    Define any derived or convenience queries against your state.

    import { queryDb } from "@livestore/livestore";
    import { tables } from "./tables.ts";
    export const uiState$ = queryDb(tables.uiState.get(), { label: "uiState" });
  7. Schemasrc/livestore/schema.ts

    Assemble events and state into a complete LiveStore schema.

    import { makeSchema, State } from "@livestore/livestore";
    import { events } from "./events.ts";
    import { materializers } from "./materializers.ts";
    import { tables } from "./tables.ts";
    export const schema = makeSchema({
    events,
    state: State.SQLite.makeState({ tables, materializers }),
    });
  8. Client workersrc/livestore/worker.ts

    Create a tiny web worker that connects the client to the Cloudflare sync backend.

    import { makeWorker } from "@livestore/adapter-web/worker";
    import { makeCfSync } from "@livestore/sync-cf";
    import { schema } from "./schema.ts";
    makeWorker({
    schema,
    sync: {
    backend: makeCfSync({ url: import.meta.env.VITE_LIVESTORE_SYNC_URL }),
    initialSyncOptions: { _tag: "Blocking", timeout: 5000 },
    },
    });
  9. Cloudflare sync backendsrc/livestore/server.ts

    Expose the LiveStore Cloudflare Worker and Durable Object for real-time sync.

    import { makeDurableObject, makeWorker } from "@livestore/sync-cf/cf-worker";
    export class WebSocketServer extends makeDurableObject({
    onPush: async (message) => {
    console.log("onPush", message.batch);
    },
    onPull: async (message) => {
    console.log("onPull", message);
    },
    }) {}
    export default makeWorker({
    validatePayload: (payload: any) => {
    if (payload?.authToken !== "insecure-token-change-me") {
    throw new Error("Invalid auth token");
    }
    },
    enableCORS: true,
    });
  10. Describe infrastructurealchemy.run.ts

    import alchemy from "alchemy";
    import { D1Database, DurableObjectNamespace, Vite, Worker } from "alchemy/cloudflare";
    const app = await alchemy("cloudflare-livestore");
    // Cloudflare Worker hosting the sync backend
    const server = await Worker("server", {
    entrypoint: "src/livestore/server.ts",
    compatibility: "node",
    bindings: {
    DB: await D1Database("db", {
    name: `${app.name}-${app.stage}-livestore`,
    adopt: true
    }),
    WEBSOCKET_SERVER: DurableObjectNamespace("websocket-server", {
    className: "WebSocketServer"
    }),
    },
    });
    // Vite site serving your React app
    await Vite("client", {
    assets: "dist",
    env: {
    VITE_LIVESTORE_SYNC_URL: server.url!,
    },
    });
    await app.finalize();
  11. Run locally

    Terminal window
    bun alchemy dev
    # open http://localhost:8787 to see LiveStore in action
  12. Deploy to production

    Terminal window
    bun alchemy deploy

Alchemy will provision:

  • a Cloudflare Worker (with Durable Object + D1) for syncing
  • your Vite site, with VITE_LIVESTORE_SYNC_URL automatically wired up
  • Explore advanced syncing options in the LiveStore docs
  • Enable Dev-Tools (@livestore/devtools-vite) for local debugging
  • Extend the schema and events as your app grows