Files
kandev/docs/specs/canvases/system-design/collaborative-canvases.md
T
2026-09-05 18:44:43 +00:00

575 lines
22 KiB
Markdown

---
id: canvases-collaborative-canvases-design
title: Collaborative canvases system design
status: superseded
system: canvases
owners:
- canvases
created: 2026-08-25
last_updated: 2026-08-30
requirements:
- REQ-CANVASES-COLLABORATIVE-CANVASES-001
- REQ-CANVASES-COLLABORATIVE-CANVASES-002
- REQ-CANVASES-COLLABORATIVE-CANVASES-003
- REQ-CANVASES-COLLABORATIVE-CANVASES-004
- REQ-CANVASES-COLLABORATIVE-CANVASES-005
- REQ-CANVASES-COLLABORATIVE-CANVASES-006
- REQ-CANVASES-COLLABORATIVE-CANVASES-007
- REQ-CANVASES-COLLABORATIVE-CANVASES-008
- REQ-CANVASES-COLLABORATIVE-CANVASES-009
- REQ-CANVASES-COLLABORATIVE-CANVASES-010
- REQ-CANVASES-COLLABORATIVE-CANVASES-011
---
# Collaborative canvases system design
This design is not the current canvas design. It is replaced by
[Agent-authored web-app canvases](agent-authored-web-apps.md).
Its blank native-canvas dialog, import flow, and prohibition on sidebar
creation actions are historical. They do not constrain the replacement design.
## Design summary
Kandev stores each canvas in one workspace as an owner-only snapshot. A canvas
can link to several tasks in that workspace and appear in each task's Dockview.
Human controls and agent tools call one typed command service. The web client
shows a closed schema with native components. It never executes canvas code.
Version 1 transfers canvases through a single inert JSON file. An import always
creates an independent canvas. Version 1 has no user sharing, invitations,
collaborator roles, presence, or live instance federation.
This design follows
[the server-owned declarative canvas decision](../../../decisions/2026-08-25-server-owned-declarative-canvases.md).
## System boundary
Add `apps/backend/internal/canvas/` as the owner of canvas storage, workspace
scope, task links, actions, events, leases, export, and import. Other systems
can store a canvas identifier, but they do not store canvas blocks.
The initial adapter boundary is:
```text
HTTP handlers ---------+
WebSocket gateway -----+--> canvas access + command service --> repository
Kandev MCP tools ------+ |
+--> export/import codec
+--> ordered owner events
```
The owner client receives agent changes through the WebSocket gateway. No
other user can subscribe to the canvas.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-001,
REQ-CANVASES-COLLABORATIVE-CANVASES-003,
REQ-CANVASES-COLLABORATIVE-CANVASES-005, and
REQ-CANVASES-COLLABORATIVE-CANVASES-008.
## Persistence
Use five canvas-owned tables.
### `canvases`
- `id`
- `owner_user_id`
- `workspace_id`
- `title`
- `schema_version`
- `revision`
- `compacted_through_revision`
- nullable `source_export_id`
- nullable `imported_at`
- nullable `archived_at`
- `created_at` and `updated_at`
### `canvas_task_links`
- `canvas_id`
- `task_id`
- `linked_by_user_id`
- `created_at`
The primary key is `(canvas_id, task_id)`. A service check requires both records
to use the same workspace. Task removal cascades only to this table. The canvas
remains.
### `canvas_blocks`
- `canvas_id`
- `block_id`
- `block_type`
- `position`
- `state_json`
- `block_revision`
- timestamps
The primary key is `(canvas_id, block_id)`. A stable sortable position avoids a
full rewrite after one block move.
### `canvas_events`
- `canvas_id`
- `revision`
- unique `command_id`
- `actor_kind` as `human` or `agent`
- nullable `actor_user_id`
- nullable `actor_session_id`
- `action`
- nullable `target_block_id`
- bounded `payload_json`
- `created_at`
The primary key is `(canvas_id, revision)`.
### `canvas_command_receipts`
- `command_id`
- `canvas_id`
- normalized result
- resulting revision
- `created_at`
Receipts preserve idempotency after event compaction. Canvas removal deletes
all canvas-owned rows. An import creates one canvas and its blocks in one
transaction. It does not create events from the source file.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-001,
REQ-CANVASES-COLLABORATIVE-CANVASES-003,
REQ-CANVASES-COLLABORATIVE-CANVASES-006, and
REQ-CANVASES-COLLABORATIVE-CANVASES-011.
## Block and action schema
The backend owns one versioned registry.
| Block | State | Main actions |
| --------- | ----------------------------------- | ------------------------------- |
| Markdown | sanitized Markdown source | replace content |
| Checklist | ordered items with stable IDs | add, edit, toggle, move, remove |
| Kanban | columns and cards with stable IDs | add, edit, move, remove |
| Metrics | named values with units and status | set, remove, reorder |
| Timeline | ordered events with time and status | add, edit, move, remove |
The registry validates all state before a write or import transaction. Version
1 uses these limits:
- 100 active canvases per owner
- 50 blocks per canvas
- 500 structured items per canvas
- 256 KiB of current block state per canvas
- 32 KiB per command input
- 512 KiB per `.kandev-canvas` file
- 200 characters per title or item label
- 1,000 retained events after compaction
One typed backend contract owns these limits. HTTP, WebSocket, MCP, export, and
import tests use the same values.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-002,
REQ-CANVASES-COLLABORATIVE-CANVASES-006,
REQ-CANVASES-COLLABORATIVE-CANVASES-010, and
REQ-CANVASES-COLLABORATIVE-CANVASES-011.
## Command transaction
The canonical command is:
```json
{
"command_id": "uuid",
"canvas_id": "uuid",
"base_revision": 41,
"action": "checklist.toggle",
"target_block_id": "uuid",
"input": { "item_id": "uuid", "completed": true }
}
```
The service does these steps in one transaction:
1. Load the canvas and trusted actor context.
2. Authorize the owner, task agent, or Office agent.
3. Return the stored receipt for a duplicate command.
4. Validate the action, input, limits, and preconditions.
5. Apply the typed change to the block snapshot.
6. Increment the canvas and block revisions.
7. Append the attributed event and command receipt.
8. Commit, then publish to the owner's authorized subscribers.
A failed transaction publishes nothing.
Structured actions contain item preconditions. A stale canvas revision is
valid when the target item still matches. Otherwise, the service returns
`canvas_conflict` with the current block and revisions.
A Markdown replacement requires the current block revision and a lease. The
bounded lease manager keys each lease by canvas, block, and actor. A lease lasts
30 seconds and can renew. A restart removes all leases safely.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-003 and
REQ-CANVASES-COLLABORATIVE-CANVASES-004.
## Portable file contract
Version 1 uses one UTF-8 JSON file with the `.kandev-canvas` extension. It is
not a ZIP file because all version 1 blocks contain bounded structured data.
The top-level contract is:
```json
{
"format": "kandev.canvas",
"format_version": 1,
"export_id": "uuid",
"exported_at": "2026-08-26T12:00:00Z",
"canvas": {
"title": "Release readiness",
"schema_version": 1,
"blocks": []
}
}
```
The codec emits fields in a deterministic order. The file contains no internal
canvas ID, block ID, user ID, task link, event, actor, session, server address,
repository data, file content, or credential. Each exported block uses a
portable position and typed state without its internal identifier.
The import codec must parse with unknown-field rejection. It validates the
format version, schema version, block registry, text encoding, and all limits.
It completes validation before it starts the transaction.
The importer creates new canvas and block identifiers. It records `export_id`
as `source_export_id` for local provenance. It assigns the current user and
selected workspace. The user can select a same-workspace task link before the
transaction.
Each import creates a new fork, even when `source_export_id` already exists.
There is no update, merge, refresh, or synchronization action in version 1.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-006 and
REQ-CANVASES-COLLABORATIVE-CANVASES-010.
## HTTP API
Add `/api/v1/canvases` endpoints for:
- workspace-scoped list, create, fetch, rename, archive, restore, and remove
- task link list, create, and remove
- block list and initial snapshot
- Markdown lease acquire, renew, and release
- `GET /api/v1/canvases/{id}/export`
- `POST /api/v1/canvases/import` with a bounded file and optional task ID
The export response uses a safe file name and
`application/vnd.kandev.canvas+json`. The import response returns the new
canvas ID and direct route. Both endpoints require the current owner identity.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-001,
REQ-CANVASES-COLLABORATIVE-CANVASES-006,
REQ-CANVASES-COLLABORATIVE-CANVASES-007, and
REQ-CANVASES-COLLABORATIVE-CANVASES-010.
## Access policy
Version 1 has one canvas role: owner.
- The authenticated workspace owner can read, change, export, link, archive,
and remove the canvas.
- The synthetic user has the same capability when authentication is disabled.
- A task agent can read or change only canvases linked to its trusted task.
- An Office agent can use canvases in its trusted workspace context.
- Another Kandev user has no canvas access.
- Import creates a new canvas in the selected owned workspace.
The canvas service uses the task system's access decision for every task-link
operation. Canvas ownership never grants access to a linked task or repository.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-007 and
REQ-CANVASES-COLLABORATIVE-CANVASES-008.
## WebSocket protocol
Extend the gateway with a canvas subscription map and an authorization callback.
Do not route canvas traffic through workspace broadcasts.
Client actions:
- `canvas.subscribe` with `canvas_id` and `after_revision`
- `canvas.unsubscribe`
- `canvas.command`
Server events:
- `canvas.snapshot`
- `canvas.event`
- `canvas.lease`
- `canvas.error` with a stable code and safe recovery data
Subscription authorization requires the current owner. Every publish path
repeats the access decision as a backstop. Disconnect removes leases owned by
that socket. Version 1 has no presence or focus messages.
The server replays events after `after_revision` when the event range exists.
Otherwise, it sends a snapshot. Clients apply only the next revision and
request recovery after a gap.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-005 and
REQ-CANVASES-COLLABORATIVE-CANVASES-011.
## Agent tools
Add a `canvas` MCP profile group for Task and Office task surfaces:
- `list_canvases_kandev`
- `create_canvas_kandev`
- `get_canvas_kandev`
- `apply_canvas_action_kandev`
The MCP server derives the actor from its backend connection. Tool schemas do
not expose another actor, export, import, task unlink, canvas removal, arbitrary
URL, or raw code input.
Task sessions list only canvases linked to their task. A task-created canvas
links to that task. Office sessions list canvases for their trusted workspace.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-007 and
REQ-CANVASES-COLLABORATIVE-CANVASES-008.
## Desktop information architecture
Add Canvases as a first-party workspace sidebar section. Model it on
`AutomationsSection` and `IntegrationsSection`. The section starts folded,
shows the workspace canvas count, and fetches rows only for the active
workspace.
The section header toggles the rows. A persistent header shortcut opens the
workspace Canvases settings page. Each row opens the canvas direct route. The
sidebar contains no creation or import actions.
An empty section shows Set up a canvas. This row opens the workspace Canvases
settings page.
Routes:
- `/settings/workspaces/:workspaceId/canvases` manages workspace canvases
- `/canvases/:canvasId` shows the focused canvas
Add `canvases` to `WORKSPACE_SETTINGS_TABS`. The settings tree and workspace
tab strip derive the row from that catalog. The workspace page follows the
Automations list page and Integrations index page patterns.
The task surface registers a first-party `canvas` Dockview component. Opening a
linked canvas reuses the same canvas surface and store as the direct route.
### Sidebar preview
```text
+----------------------------+
| KANDEV |
| |
| [ ] Home |
| [ ] Tasks |
| |
| [#] Canvases v 3 [=] |
| Release readiness |
| Incident 482 |
| API migration |
| |
| [ ] Stats |
| [ ] Settings |
+----------------------------+
```
The number is the workspace canvas count. The `[=]` header shortcut opens
workspace settings. The expanded list uses the Automations row density. Each
row opens the direct route.
### Workspace settings preview
```text
+--------------------------------------------------------------+
| Workspace settings > Main workspace > Canvases |
+--------------------------------------------------------------+
| Canvases |
| Create, import, and manage canvases for this workspace. |
| [Import] [New canvas] |
+--------------------------------------------------------------+
| Search [_______________________________________________] |
| |
| Name Linked tasks Updated Actions |
| Release readiness Prepare release 4 minutes ago [...] |
| Incident 482 2 tasks Yesterday [...] |
| API migration No tasks 12 Aug [...] |
+--------------------------------------------------------------+
```
The page owns creation and import. Row actions contain Open, Export, Archive,
and Remove. The table becomes a vertical card list on phones.
### New canvas preview
```text
+------------------------------------------------+
| New canvas [x] |
| |
| Title |
| [ Release readiness ] |
| |
| Link to task (optional) |
| [ No task v] |
| |
| The canvas also appears in the Canvases list. |
| |
| [Cancel] [Create canvas] |
+------------------------------------------------+
```
The workspace settings action leaves the task option empty. A task Dockview
action selects the current task by default. The dialog creates a blank canvas.
Users add the first block after creation. Version 1 does not include templates.
### Dockview panel preview
```text
+---------------- Task workbench -------------------------------+
| [Agent] [Changes] [Files] [Canvas: Release readiness] [x] |
+---------------------------------------------------------------+
| Release readiness Saved at 12:44 [Open] [Export] |
| Linked task: Prepare release 0.52 [...] |
+----------------+----------------------------------------------+
| Blocks | Checklist: Release gates [+ Add] |
| | |
| > Release gates| [x] Backend tests |
| Metrics | [x] Web typecheck |
| Timeline | [ ] Desktop package |
| | [ ] Release notes |
| + Add block | |
| | Agent updated this block at 12:43 |
+----------------+----------------------------------------------+
```
The panel header contains Open full canvas, Export, and an overflow menu. The
overflow menu contains Rename, task links, Archive, and Remove. The canvas
body owns internal scrolling.
## Import interaction
The Import action on the workspace Canvases settings page opens a file picker
for `.kandev-canvas`. Kandev parses the file before it shows the preview.
The preview shows title, block count, block types, format version, file size,
and an optional task link. It does not render block content before full
validation. The primary action is Import as new canvas.
A successful import opens the new direct route. The interface states that the
new canvas is an independent copy with no synchronization.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-006 and
REQ-CANVASES-COLLABORATIVE-CANVASES-009.
## Mobile design contract
- **Outcome and entry:** users open the workspace Canvases settings page from
mobile settings navigation. A task can open the focused canvas route.
- **Exemplars:** use `task-layout.tsx` for full-height ownership. Use
`mobile-picker-sheet.tsx` for the block and action drawer. Use
`kanban-with-preview.tsx` for direct detail navigation.
- **Hierarchy:** show the canvas title, save state, focused block, and primary
block action. Put secondary canvas actions in an overflow control.
- **Surface:** use a direct `h-dvh` route. Do not mount Dockview on a phone.
- **Navigation:** use vertical cards on the workspace page. Keep New canvas and
Import visible as 44-pixel actions. Use an inset drawer for editor blocks,
task links, export, archive, and remove.
- **Geometry:** use 44 by 44 CSS pixel action targets and safe-area padding.
Use one internal vertical scroll owner. Prevent page-level horizontal scroll.
- **Dense content:** show one Kanban column at a time. Wrap metrics. Keep
checklist and timeline content vertical.
Desktop and mobile use the same API, store, command hooks, file codec responses,
and access decisions. All user copy uses translation keys in every required
locale.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-009.
## Compaction and recovery
After a command leaves more than 1,000 events, a bounded operation removes the
oldest events and advances `compacted_through_revision`. The block rows already
contain the current snapshot.
A client older than the compaction revision receives the full snapshot. A
client inside the retained range receives ordered events. Command receipts
preserve idempotency after event removal.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-003,
REQ-CANVASES-COLLABORATIVE-CANVASES-005, and
REQ-CANVASES-COLLABORATIVE-CANVASES-011.
## Observability
Add content-free metrics for:
- command result
- event or snapshot recovery
- active owner subscriptions
- lease result
- export, import, and rejected-import results
Logs can include canvas ID, command ID, action, revision, actor kind, format
version, file size, and a safe error code. They must omit titles, block state,
Markdown, task data, file content, and user-supplied file names.
This maps REQ-CANVASES-COLLABORATIVE-CANVASES-010 and
REQ-CANVASES-COLLABORATIVE-CANVASES-011.
## Failure handling
- Duplicate commands return the stored receipt.
- Conflicts return the current target state and revisions.
- A busy Markdown block returns safe lease state.
- A lost connection leaves local state visible and marks it offline.
- Editing remains disabled until recovery is complete.
- A non-owner request returns one generic forbidden error.
- An invalid import returns one safe reason and creates no rows.
- An unknown schema version shows an unsupported-state screen.
- Export failure does not change canvas state.
## Test strategy
- Repository tests cover migrations, task links, removal, compaction, receipts,
and database replay.
- Service tests cover block actions, limits, owner access, task-agent access,
conflicts, leases, export, and atomic import.
- Codec tests cover deterministic output, excluded fields, new identifiers,
repeated import, unknown fields, versions, and limits.
- Gateway tests cover owner authorization, event order, replay, snapshots, and
lease cleanup.
- MCP tests cover trusted actor context and the absence of portability tools.
- Frontend tests cover the folded sidebar, settings catalog, workspace page,
creation, import preview, recovery, conflicts, Dockview, and mobile layout.
- Desktop Playwright covers workspace settings, creation, task links, Dockview,
agent updates, export, import, reload, restart, and archive behavior.
- Mobile Playwright covers navigation, creation, import, block selection,
export, 44-pixel targets, safe areas, and zero horizontal overflow.
## Requirement traceability
| Requirement | Main design sections |
| --------------------------------------- | ---------------------------------------------------------------------------- |
| REQ-CANVASES-COLLABORATIVE-CANVASES-001 | Persistence, HTTP API, Desktop information architecture |
| REQ-CANVASES-COLLABORATIVE-CANVASES-002 | Block and action schema |
| REQ-CANVASES-COLLABORATIVE-CANVASES-003 | Persistence, Command transaction, Compaction and recovery |
| REQ-CANVASES-COLLABORATIVE-CANVASES-004 | Command transaction, Failure handling |
| REQ-CANVASES-COLLABORATIVE-CANVASES-005 | WebSocket protocol, Compaction and recovery |
| REQ-CANVASES-COLLABORATIVE-CANVASES-006 | Portable file contract, HTTP API, Import interaction |
| REQ-CANVASES-COLLABORATIVE-CANVASES-007 | Access policy, Agent tools |
| REQ-CANVASES-COLLABORATIVE-CANVASES-008 | Access policy, Agent tools |
| REQ-CANVASES-COLLABORATIVE-CANVASES-009 | Desktop information architecture, Import interaction, Mobile design contract |
| REQ-CANVASES-COLLABORATIVE-CANVASES-010 | Block and action schema, Portable file contract, Observability |
| REQ-CANVASES-COLLABORATIVE-CANVASES-011 | Persistence, WebSocket protocol, Compaction and recovery, Observability |