ADR: Shared Workspace Content vs Per-User View State
- Status: Proposed
- Date: 2026-03-21
Context
The current frontend Workspaces implementation stores everything in one browser-local workspace
document, including:
- metadata such as title, description, and labels
- grid layout and widget configuration
- selected controls state
- widget runtime state such as zoom, pan, selected node, and minimap visibility
That approach works for single-user browser-local development, but production workspaces are expected to be:
- shared across users
- editable by users with
can_edit - readable by users with
can_view - copyable for reuse and templating
If all state is stored in one shared document, personal interactions would overwrite each other.
Examples:
- one user changing the date range would change it for everyone
- one user panning or zooming a graph would overwrite another user's view
- one user selecting a node in a graph would change the open context for all viewers
Decision
We split workspace persistence into four concepts:
-
WorkspaceStore shared, collaborative workspace content only:- title
- description
- labels
- grid
- shared controls configuration
- widgets
- widget props
-
WorkspaceUserStateStore personal, non-collaborative view state:- selected control values
- widget runtime state
- last-opened view
- last-opened timestamp
-
WorkspaceUserGrantandWorkspaceTeamGrantStore object-level access withview/editsemantics for direct users and teams. -
WorkspaceRevisionStore immutable shared-content snapshots for:- copy
- import/export
- history
- recovery
Why we are doing it this way
Shared objects should share content, not temporary viewport state
The workspace itself is a collaborative artifact. Widget configuration, layout, labels, and metadata are part of the shared artifact and should be visible to everyone with access.
But temporary viewport choices are not collaborative content. They are personal interaction state.
Putting both categories in the same stored document would create false collaboration and constant last-write-wins conflicts.
The frontend model already distinguishes durable config from runtime state
The widget contract already separates:
- widget
props - widget
runtimeState
That same distinction should exist in the backend. props belong to the shared workspace content;
runtimeState belongs to the user's current view of that content.
Workspaces need object-level sharing
Production workspaces are not just user preferences. They are shareable objects with RBAC.
That means the backend must support:
- direct user grants
- team grants
viewvseditaccess levels
This is much easier to query, audit, and maintain with explicit grant tables than with a single opaque JSON ACL field.
Copy and history need immutable snapshots
Copying a workspace should copy the shared content only.
It should not copy:
- another user's transient zoom/pan
- another user's current selected date range
- another user's current graph selection
Using WorkspaceRevision snapshots keeps copy/import/history aligned around one shared-content
representation.
Consequences
Positive
- shared workspace edits are deterministic
- personal viewing state no longer clobbers collaborators
- copy/import/export become clean shared-content operations
- RBAC stays explicit and queryable
- revision history becomes straightforward
Tradeoffs
- backend APIs must load and save two state categories instead of one
- the application layer must validate JSON document structure for widgets and controls
- effective permission resolution must combine direct user grants and team grants
Non-decisions
This ADR does not decide:
- whether workspaces are version-published or autosaved
- whether revisions are created on every save or only on major events
- whether favorites belong in the workspace API or in the general shell preferences API
Rejected alternatives
Store everything in one shared workspace row
Rejected because it makes temporary per-user interactions collaborative by accident.
Do not store any per-user workspace state
Rejected because it would force every user to lose useful personal viewing state across sessions, even though that state clearly exists and is meaningful in the frontend.
Normalize every widget instance into relational tables
Rejected for v1 because widget props and runtime state are heterogeneous by widget type, and the workspace is already treated as a document in the frontend and import/export flow.