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.
-
Install dependencies
Terminal window bun add alchemy @livestore/livestore @livestore/adapter-web @livestore/react @livestore/sync-cf react react-dom viteTerminal window npm install alchemy @livestore/livestore @livestore/adapter-web @livestore/react @livestore/sync-cf react react-dom viteTerminal window pnpm add alchemy @livestore/livestore @livestore/adapter-web @livestore/react @livestore/sync-cf react react-dom viteTerminal window yarn add alchemy @livestore/livestore @livestore/adapter-web @livestore/react @livestore/sync-cf react react-dom vite -
Create folder structure
Create
src/livestore/
and add the following files. -
Tables –
src/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",},},}),}; -
Events –
src/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,}; -
Materializers –
src/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 }),}); -
Queries –
src/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" }); -
Schema –
src/livestore/schema.ts
Assemble
events
andstate
into a complete LiveStoreschema
.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 }),}); -
Client worker –
src/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 },},}); -
Cloudflare sync backend –
src/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,}); -
Describe infrastructure –
alchemy.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 backendconst 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 appawait Vite("client", {assets: "dist",env: {VITE_LIVESTORE_SYNC_URL: server.url!,},});await app.finalize(); -
Run locally
Terminal window bun alchemy dev# open http://localhost:8787 to see LiveStore in action -
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
Next steps
Section titled “Next steps”- 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