[]previous and next page

Saving and storage

Autosave in one line, store documents wherever you like, keep images out of the document, and know what is saved when.

Autosave in one line#

ts
createCanvasFlow(document.getElementById('board')!, { persistenceKey: 'board-42' })

With a persistenceKey, the web host saves the document to IndexedDB (falling back to localStorage) a moment after each change, restores it on load, and writes a last copy when the page is closing. It also remembers the person's own view of the board, the camera and current page, separately from the document.

Your own storage#

Storage is a three-method interface. Anything with get and set works: localStorage, IndexedDB, AsyncStorage, a REST API, a file.

ts
interface StorageAdapter {
  get(key: string): Promise<string | null> | string | null
  set(key: string, value: string): Promise<void> | void
  remove?(key: string): Promise<void> | void
  setSync?(key: string, value: string): void // a best-effort write while the page is closing
}

Pass one to the host:

api-storage.tsts
import type { StorageAdapter } from 'canvasflow'
import { createCanvasFlow } from 'canvasflow/dom'

const apiStorage: StorageAdapter = {
  async get(key) {
    const res = await fetch(`/api/boards/${key}`)
    return res.ok ? res.text() : null
  },
  async set(key, value) {
    await fetch(`/api/boards/${key}`, { method: 'PUT', body: value })
  },
}

createCanvasFlow(document.getElementById('board')!, { persistenceKey: 'board-42', storage: apiStorage })

The built-in adapters are indexedDBAdapter() and localStorageAdapter() from canvasflow/dom, and memoryStorage() from canvasflow for tests and previews.

The persistence plugin#

On other hosts, or when you want more control, use the plugin directly:

persist.tsts
import { createEditor, memoryStorage, persistencePlugin } from 'canvasflow'

const persistence = persistencePlugin({
  key: 'board-42',
  storage: memoryStorage(),
  delay: 400, // ms after the last change
  onStatus: (state) => console.log(state), // 'loading' | 'saving' | 'saved' | 'error'…
  onLoaded: (found) => console.log(found ? 'restored' : 'new board'),
})

const editor = createEditor({ plugins: [persistence] })
await persistence.controller.flush() // save now, for example before navigating away
Option What it does
key The document's storage key
storage Where the document goes
sessionStorage Where the person's view (camera, page) goes. Defaults to storage
sessionKeys Which parts of the instance state to remember per document
delay Debounce in milliseconds
onStatus, onLoaded Callbacks for a "Saved" indicator and first-load decisions

Images and other files#

By default, images are embedded in the document as data URLs. That's perfect for local-only boards and terrible for shared ones: documents grow fast, and every collaborator downloads every image on every load.

Give the editor an asset store, and images are uploaded once and referenced by URL:

assets.tsts
import type { AssetStore } from 'canvasflow'

export const s3Assets: AssetStore = {
  async upload(file, info) {
    const { url } = await fetch('/api/upload-url', { method: 'POST', body: JSON.stringify(info) }).then((r) => r.json())
    await fetch(url, { method: 'PUT', body: file })
    return { src: url.split('?')[0] }
  },
  // Optional: turn a stored src into something displayable (signed URLs, a CDN).
  resolve: (src) => src.replace('s3.amazonaws.com/my-bucket', 'cdn.example.com'),
}
ts
createCanvasFlow(document.getElementById('board')!, { assets: s3Assets })

info carries the file's name, mimeType, w and h. Dropping, pasting and the image tool all go through your store.

Snapshots by hand#

Sometimes you just want the data:

ts
const snapshot = editor.getSnapshot()
await fetch('/api/boards/42', { method: 'PUT', body: JSON.stringify(snapshot) })

editor.loadSnapshot(await fetch('/api/boards/42').then((r) => r.json()))

Snapshots carry a schema version and are migrated forward on load. See Records and the store.

Saving and collaboration together#

When a board is shared, let the collaboration layer be the source of truth and give it an offline cache instead of autosaving the document separately. Edits made offline are kept, with their merge information, and merge correctly when the connection returns.