Skip to content

Files, presence & broadcast

const file = await db.$files.upload(blob, { path: "avatars/me.png" });
const bytes = await db.$files.download("avatars/me.png");
const all = await db.$files.list({ prefix: "avatars/" });
await db.$files.delete("avatars/me.png");

File bytes live in the FILES R2 bucket, with a 25 MB cap and member read / editor write. Their index is written through the transaction log, so a restore keeps it.

Presence is ephemeral and rides the workspace socket:

const room = db.$presence("board:" + boardId);
room.set({ name, color });
room.cursor({ x, y });
const stop = room.subscribe((peers) => render(peers));
// When leaving the room:
room.leave();

A presence room also exposes drag() and clearDrag() for drag state.

Broadcast is a fire-and-forget message between the clients of a workspace, on the same socket:

const board = db.$broadcast<{ kind: "ping"; at: number }>("board:" + boardId);
const stop = board.subscribe((message, from) => flash(from.userId, message));
board.send({ kind: "ping", at: Date.now() }); // false (dropped) while offline

A refused topic subscription reports through the optional onError callback on subscribe. A message reaches every other client listening on the topic, with the sender’s client id and user id; your own messages are not echoed back. Nothing is stored or replayed, and a send while the socket is down is dropped rather than delivered late. Listening clients rejoin their topics after a reconnect.

By default any member of the workspace may listen and send. Decide per topic on the Worker:

import { defineTapeDB, WORKSPACE_ROLE_LEVELS } from "tapedb/worker";
import schema from "./tapedb.schema";
defineTapeDB({
schema,
topics: {
authorize: ({ auth, topic, action }) =>
!topic.startsWith("admin:") || auth.role >= WORKSPACE_ROLE_LEVELS.ADMIN,
},
});

React bindings: usePresence and useBroadcast in React.