API Reference
O apps/tldraw-sync/ é um Cloudflare Worker que habilita a edição colaborativa em tempo real nos boards. Diferente das notas (que usam Hocuspocus + Y.js), o canvas usa o protocolo nativo do tldraw (TLSync) sobre WebSocket.
Como funciona
Cada board tem sua própria instância de TldrawDurableObject — um Durable Object da Cloudflare que mantém o estado de sync em memória e persiste as shapes em SQLite embutido.
Navegador A ──┐
├── WebSocket ── Cloudflare Worker ── TldrawDurableObject (SQLite)
Navegador B ──┘
│
└── R2 Bucket (assets: imagens, mídia)
O Worker valida um JWT de board antes de fazer o upgrade de WebSocket, e então roteia para o DO via idFromName("board-{boardId}") — garantindo que clientes do mesmo board sempre chegam à mesma instância.
Estrutura de arquivos
apps/tldraw-sync/
├── src/
│ ├── worker.ts # entry point — roteamento HTTP/WS e handlers de upload
│ ├── TldrawDurableObject.ts # DO: TLSocketRoom + SQLiteSyncStorage
│ ├── auth.ts # verificação do JWT de board (jose)
│ ├── env.ts # tipagem dos bindings Cloudflare
│ └── schema.ts # schema tldraw (tipos de shapes suportados)
├── wrangler.toml # bindings: DO, R2, vars, migrations
├── package.json
└── tsconfig.json
Durable Object
TldrawDurableObject é instanciado uma vez por board via idFromName. As shapes são persistidas no SQLite embutido do DO através de SQLiteSyncStorage. O TLSocketRoom gerencia o protocolo TLSync em memória e é descartado quando o último cliente desconecta — os dados continuam no SQLite.
// apps/tldraw-sync/src/TldrawDurableObject.ts
export class TldrawDurableObject extends DurableObject<Env> {
private room: TLSocketRoom<TLRecord, void> | null = null;
private getOrCreateRoom() {
return this.ctx.blockConcurrencyWhile(async () => {
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage);
const storage = new SQLiteSyncStorage<TLRecord>({ sql });
this.room = new TLSocketRoom({ schema, storage, clientTimeout: Infinity });
return this.room;
});
}
}
Sem WebSocket Hibernation API: a versão
@tldraw/[email protected]não expõe API de resume de sessão pós-hibernação. O DO usaserver.accept()— não hiberna enquanto há conexões abertas. Para boards ociosos, o Cloudflare evita o DO normalmente.
Autenticação
O JWT é gerado pelo app web (GET /api/collab/board-token) e verificado pelo Worker em toda requisição autenticada:
// apps/tldraw-sync/src/auth.ts
export async function verifyBoardToken(token: string, secret: string) {
const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
return payload as BoardTokenPayload;
}
O token vai como query string no WebSocket (?token=...) porque browsers não permitem headers customizados no new WebSocket(). Em uploads de assets vai no header Authorization: Bearer.
R2 — Assets do canvas
Imagens arrastadas para o canvas são gravadas num R2 bucket (locus-tldraw-assets) separado do bucket principal. O upload é autenticado — o Worker valida que o boardId do caminho bate com o boardId do JWT antes de gravar. O serving é público com Cache-Control: immutable.
Relação com o app web
O app web nunca toca no conteúdo do canvas — apenas gerencia metadados (título, ícone, descrição) no PostgreSQL via Prisma. O conteúdo das shapes vive exclusivamente no SQLite do Durable Object.
| Dado | Onde fica | Quem gerencia |
|---|---|---|
| Metadados do board | PostgreSQL | apps/web |
| Shapes do canvas | SQLite do DO | apps/tldraw-sync |
| Imagens/assets | R2 | apps/tldraw-sync |
Variáveis de ambiente
# apps/web/.env
NEXT_PUBLIC_TLDRAW_SYNC_WS_URL="ws://127.0.0.1:8787" # dev
COLLAB_JWT_SECRET="..." # deve ser igual ao do Worker
# apps/tldraw-sync — wrangler secret put
COLLAB_JWT_SECRET="..."
Desenvolvimento local
# Terminal 1 — Worker (porta 8787, R2 e DO simulados localmente)
cd apps/tldraw-sync && pnpm dev
# Terminal 2 — Web app
cd apps/web && pnpm dev