Sync with Convex
Use your own Convex deployment as the realtime backend and the database for CanvasFlow documents, with your own access rules.
canvasflow/convex turns a Convex deployment into a CanvasFlow sync server: live edits, presence, persistence and offline catch-up, with your authorization deciding who can read and write each room. It doesn't depend on Convex itself: you pass in Convex's builders, so it works with whatever version you have.
Add the tables#
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
import { canvasflowTables } from 'canvasflow/convex'
export default defineSchema({
...canvasflowTables({ defineTable, v }),
// your own tables
})
This adds three tables: the change log, compacted snapshots, and presence.
Export the functions#
import { v } from 'convex/values'
import { mutation, query } from './_generated/server'
import { canvasflowFunctions } from 'canvasflow/convex'
export const { load, pull, push, presence, setPresence, leave } = canvasflowFunctions({
query,
mutation,
v,
authorize: async (ctx, room, access) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Sign in to continue')
// look up the board for `room` and check that this person may `access` ('read' or 'write') it
},
})
authorize runs for every read and write. Throw to deny.
Connect the client#
import { ConvexClient } from 'convex/browser'
import { CollabClient } from 'canvasflow/collab'
import { ConvexTransport } from 'canvasflow/convex'
import { api } from '../convex/_generated/api'
const convex = new ConvexClient(import.meta.env.VITE_CONVEX_URL)
const transport = new ConvexTransport(convex, api.canvasflow, boardId)
const client = new CollabClient(editor, transport, { room: boardId }).start()
With ConvexReactClient#
ConvexTransport needs query, mutation and onUpdate. The browser ConvexClient has all three. If your app uses ConvexReactClient, adapt it in a few lines:
import type { ConvexReactClient } from 'convex/react'
import type { ConvexClientLike } from 'canvasflow/convex'
export function toCanvasFlowClient(client: ConvexReactClient): ConvexClientLike {
return {
query: (fn, args) => client.query(fn, args),
mutation: (fn, args) => client.mutation(fn, args),
onUpdate(fn, args, callback, onError) {
const watch = client.watchQuery(fn, args)
const read = () => {
try {
const value = watch.localQueryResult()
if (value !== undefined) callback(value)
} catch (error) {
onError?.(error as Error)
}
}
const unsubscribe = watch.onUpdate(read)
read()
return unsubscribe
},
connectionState: () => ({ isWebSocketConnected: client.connectionState().isWebSocketConnected }),
}
}
Options#
| Option | Default | What it does |
|---|---|---|
authorize(ctx, room, access) |
allow all | Your access rule. Throw to deny |
compactEvery |
400 |
Fold the log into a snapshot every N entries |
presenceTtl |
30000 |
Ignore presence older than this (ms) |
maxPayload |
900 KB |
Largest push accepted. Convex documents cap at 1 MB |
How it stores a board#
Each edit is appended to an ordered log for its room. Every compactEvery entries, the log is merged into one snapshot, using the same CRDT the clients use, so loading a board reads one snapshot plus a short tail. Clients that were offline during a compaction notice and reload cleanly.
Images#
Keep images in Convex file storage with an asset store: upload through a mutation that returns an upload URL, and resolve stored ids to URLs. The document then holds references, not image data.