You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add optional server-backed workspace snapshot storage using a configured ClickHouse table.
The existing local workspace repository remains the live editing and autosave store. This feature adds two explicit Workbench File-menu operations:
Save workspace to server…
Load workspace from server…
Save appends an immutable version row. Load searches the server catalog, lets the user select one of the ten latest versions, validates the stored portable bundle, and transactionally replaces the current local workspace contents.
There is no app-specific backend. The browser reads and writes the configured table directly through the current authenticated ClickHouse connection.
Goals
Store shareable, versioned workspace snapshots in ClickHouse.
Keep every historic version as an immutable row.
Use one row per version of one Dashboard-bearing workspace.
Search latest Dashboard heads by user-visible name/title.
Display the latest ten versions with timestamp, saver, counts, note, and a compact version tag.
Reuse the canonical portable-bundle codec and transactional workspace import/replace pipeline.
Make the feature deployment-configurable per ClickHouse server.
Non-goals
Replacing the local IndexedDB workspace repository.
Automatic background synchronization or autosave to ClickHouse.
Collaborative editing, merge/conflict resolution, CAS, or CRDT behavior.
Updating or deleting server versions.
Automatically creating, altering, or migrating the configured ClickHouse table.
A separate central storage server distinct from the current ClickHouse connection.
Application-managed ACLs. Visibility is controlled by ClickHouse grants and row policies.
Server storage for a workspace with no Dashboard.
Stored payload
Store canonical PortableBundleV1 JSON, not the internal StoredWorkspaceV1 representation.
Each saved payload must contain:
exactly one Dashboard document;
the complete current workspace saved-query catalog, including queries not referenced by the Dashboard;
portable bundle metadata using the current workspace name;
canonical portable-bundle encoding and validation.
The Dashboard's stable dashboard.id is the server history identity. The workspace name and Dashboard title are searchable/display metadata only and may change between versions without changing identity.
Save is unavailable when the current workspace has no Dashboard.
ClickHouse table
The configured table is append-only and must preserve every historic row. Use a plain MergeTree, not ReplacingMergeTree.
Reference schema:
CREATETABLEasb.workspace_versions
(
dashboard_id String,
version UInt64 DEFAULT generateSnowflakeID(),
saved_at DateTime64(3, 'UTC')
ALIAS snowflakeIDToDateTime64(version),
name String,
dashboard_title String,
saved_by String DEFAULT currentUser(),
app_version LowCardinality(String),
bundle_version UInt16,
query_count UInt32,
tile_count UInt32,
filter_count UInt32,
note String DEFAULT '',
payload String CODEC(ZSTD(3)),
payload_hash UInt64
MATERIALIZED cityHash64(payload)
)
ENGINE = MergeTree
ORDER BY (dashboard_id, version);
The exact database and table are deployment-configured. The application must not assume asb.workspace_versions.
Required columns
The application must require compatible columns for:
version must be generated by the table default. saved_by must be generated from currentUser() by the table default. The application does not send trusted values for either field.
saved_at and payload_hash may be aliases/materialized columns as shown, but the required listing queries must be supported.
ClickHouse requirement
Server workspace storage requires ClickHouse support for:
The presence of a valid workspace_storage object enables the feature. A separate enabled flag is unnecessary.
The table value must be parsed strictly as a qualified ClickHouse identifier, for example database.table. Quote the database and table identifiers independently. Never treat the configured value as a free-form SQL fragment.
For configured alternate hosts, allow a host-specific capability:
top-level storage applies to the serving/current-origin connection;
a configured host may provide its own storage table;
manual or unlisted connections have no server-storage capability;
storage always uses the current connected ClickHouse server and credentials;
when no storage is configured, the two server operations are absent from the File menu.
Table conformance check
Before the first Save or Load operation for a connection, validate the configured table with a lightweight metadata query such as DESCRIBE TABLE.
Cache a successful check for the connection session. A failed check must show one clear diagnostic describing the missing/incompatible column or missing table privilege.
Do not run DDL and do not attempt automatic schema migration.
File menu
When server storage is configured and valid, add:
Save workspace to server…
Load workspace from server…
Use explicit destination wording so these operations are not confused with local persistence or JSON file import/export.
Suggested grouping:
New workspace…
Server
Save workspace to server…
Load workspace from server…
File
Open workspace…
Export workspace…
Import queries…
The exact grouping may follow the current menu visual conventions, but keyboard order must match visual order.
This is a Workbench operation in the MVP. Do not expose it on detached/read-only Dashboard routes.
Save workspace flow
Opening Save shows a small dialog:
Save workspace to server
Name: [current workspace name]
Note: [optional]
Cancel Save
The name is editable metadata and defaults to the current workspace name. The note is optional.
Save sequence:
Disable duplicate submission while a request is in progress.
Flush all queued local workspace writes.
Load the latest committed local workspace.
Require a non-null Dashboard.
Build a canonical PortableBundleV1 containing exactly one Dashboard and the complete workspace query catalog.
Validate and encode the bundle before sending anything.
Use the ordinary authenticated ClickHouse POST path with the SQL and JSONEachRow payload in the request body. Multi-megabyte workspace payloads are acceptable; do not introduce a separate upload protocol or URL-parameter transport.
Every explicit successful Save creates a new version, even when the payload matches the previous version. payload_hash is diagnostic metadata, not an MVP deduplication gate.
Save failure behavior
A failed INSERT leaves the local workspace unchanged.
Validation failure prevents the request.
A ClickHouse error shows a useful table/grant/schema diagnostic.
Do not automatically retry an INSERT after an ambiguous network failure: the server may have committed even if the response was lost.
For an ambiguous completion, report that the save result is unknown and direct the user to reopen server history.
Authentication refresh before the first send may follow the existing client behavior, but no generic post-send retry may create another version automatically.
Search and Load flow
Open a dialog with a name/title search field:
Load workspace from server
Name or Dashboard title: [________________] [Search]
Search returns up to ten latest Dashboard heads. Name is not an identity and duplicate names are allowed.
Each result shows enough metadata to distinguish duplicates:
Query Log Explorer
Latest: 22 Jul 2026 16:42 · alice · 18 queries · version 7F2A
Dashboard ID: qle-dashboard
The Dashboard ID may be visually muted or shown only when needed, but selection is always by stable dashboard_id.
Latest-head query
Use one tuple-valued argMax so all displayed metadata comes from the same latest row:
SELECT*FROM
(
SELECT
dashboard_id,
argMax(
tuple(
name,
dashboard_title,
version,
saved_by,
query_count,
tile_count,
filter_count,
payload_hash
),
version
) AS latest,
argMin(saved_by, version) AS created_by,
min(version) AS first_version
FROM`database`.`table`GROUP BY dashboard_id
)
WHERE
positionCaseInsensitive(latest.1, {search:String}) >0OR positionCaseInsensitive(latest.2, {search:String}) >0ORDER BYlatest.3DESCLIMIT10
The implementation may use an equivalent query, but it must not combine metadata from different versions.
Version list
After selecting a Dashboard, show its ten latest versions:
SELECT
toString(version) AS version,
right(hex(version), 4) AS version_tag,
snowflakeIDToDateTime64(version) AS saved_at,
saved_by,
name,
note,
query_count,
tile_count,
filter_count,
length(payload) AS payload_bytes
FROM`database`.`table`WHERE dashboard_id = {dashboard_id:String}
ORDER BY version DESCLIMIT10
Example UI:
22 Jul 16:42 · alice · 7F2A
Add customer filter
18 queries · 9 tiles
22 Jul 11:03 · bob · 00C4
Before layout changes
17 queries · 8 tiles
Ten versions is the MVP limit. Older versions remain in the table but do not need a Load-more UI in this issue.
Loading one version
Load by exact full version ID:
SELECT payload
FROM`database`.`table`WHERE dashboard_id = {dashboard_id:String}
AND version = {version:UInt64}
LIMIT1
Before changing local state:
fetch the selected payload;
decode it as PortableBundleV1;
require exactly one Dashboard;
run complete structural and cross-resource semantic validation;
show the existing destructive workspace-replacement confirmation;
transactionally replace the current workspace contents through the existing import planner/repository path;
adopt the selected row's name as the local workspace name;
retain the loaded Dashboard ID so a later server Save appends to the same Dashboard history.
No local workspace mutation occurs until the payload has completely decoded and validated.
Loading an older version does not update or delete server rows. A later Save appends a new version with a higher Snowflake ID under the same Dashboard ID.
Timestamp presentation
Snowflake time is authoritative. Derive it with snowflakeIDToDateTime64(version).
Store and query timestamps in UTC. Display them using the application's normal localized date/time formatting, while preserving the full instant in an accessible title or detail surface.
Security and privileges
The application relies on ClickHouse authorization.
Minimum required privileges on the configured table:
SELECT
INSERT
No UPDATE, DELETE, ALTER, or CREATE privilege is required.
Workspace bundles contain SQL and may contain sensitive literals. Deployments must choose the desired visibility model through ClickHouse grants and row policies, for example:
a shared catalog visible to all permitted users;
private rows restricted by saved_by = currentUser();
a deployment-specific team/tenant policy.
The application must not implement a second ACL system inside the payload.
Architecture
Introduce a dedicated server-workspace storage service behind an injected application/network seam.
Responsibilities should remain separated:
config parsing and capability resolution;
pure table-name parsing and SQL construction;
portable-bundle snapshot building and validation;
ClickHouse POST operations;
Workbench File-menu dialogs;
transactional projection through the existing workspace mutation/import path.
Do not make UI modules construct unescaped identifiers or raw payload SQL manually.
The local WorkspaceRepository contract remains unchanged and authoritative for live editing.
Cancellation and concurrency
Search and version-list requests should be cancellable when the dialog closes or a newer search replaces them.
Closing the dialog must not mutate workspace state.
Save prevents ordinary double submission.
A server Load enters the same local workspace write queue used by other destructive workspace operations.
A local write that completes before Save snapshot construction must be included because Save flushes the queue first.
A Load must re-confirm against the current workspace before replacement if another local mutation landed while the selection dialog was open.
Tests
Cover at least:
Configuration and schema
top-level configured store;
host-specific configured store;
no configuration hides both actions;
strict database.table identifier parsing and quoting;
Summary
Add optional server-backed workspace snapshot storage using a configured ClickHouse table.
The existing local workspace repository remains the live editing and autosave store. This feature adds two explicit Workbench File-menu operations:
Save appends an immutable version row. Load searches the server catalog, lets the user select one of the ten latest versions, validates the stored portable bundle, and transactionally replaces the current local workspace contents.
There is no app-specific backend. The browser reads and writes the configured table directly through the current authenticated ClickHouse connection.
Goals
Non-goals
Stored payload
Store canonical
PortableBundleV1JSON, not the internalStoredWorkspaceV1representation.Each saved payload must contain:
The Dashboard's stable
dashboard.idis the server history identity. The workspace name and Dashboard title are searchable/display metadata only and may change between versions without changing identity.Save is unavailable when the current workspace has no Dashboard.
ClickHouse table
The configured table is append-only and must preserve every historic row. Use a plain
MergeTree, notReplacingMergeTree.Reference schema:
The exact database and table are deployment-configured. The application must not assume
asb.workspace_versions.Required columns
The application must require compatible columns for:
versionmust be generated by the table default.saved_bymust be generated fromcurrentUser()by the table default. The application does not send trusted values for either field.saved_atandpayload_hashmay be aliases/materialized columns as shown, but the required listing queries must be supported.ClickHouse requirement
Server workspace storage requires ClickHouse support for:
Document the minimum supported ClickHouse version for these functions.
Version identity
Use the full
UInt64Snowflake ID as the immutable version identifier and chronological ordering key.Do not use UUIDs or a
max(version) + 1allocation query.The browser must treat
UInt64identifiers as decimal strings orBigInt; it must never round-trip them through JavaScriptnumber.User-facing version tag
Display the low 16 bits of the Snowflake ID as four uppercase hexadecimal digits:
The tag is a visual shorthand only. It may repeat and must never be used alone for selection or loading. All operations use the complete
UInt64value.The server query may provide the tag directly:
Configuration
Extend
config.jsonwith an optional server-storage capability:{ "workspace_storage": { "table": "asb.workspace_versions" } }The presence of a valid
workspace_storageobject enables the feature. A separateenabledflag is unnecessary.The table value must be parsed strictly as a qualified ClickHouse identifier, for example
database.table. Quote the database and table identifiers independently. Never treat the configured value as a free-form SQL fragment.For configured alternate hosts, allow a host-specific capability:
{ "hosts": [ { "label": "Production", "url": "https://clickhouse.example.com", "auth": "oauth", "workspace_storage": { "table": "asb.workspace_versions" } } ] }Rules:
Table conformance check
Before the first Save or Load operation for a connection, validate the configured table with a lightweight metadata query such as
DESCRIBE TABLE.Cache a successful check for the connection session. A failed check must show one clear diagnostic describing the missing/incompatible column or missing table privilege.
Do not run DDL and do not attempt automatic schema migration.
File menu
When server storage is configured and valid, add:
Use explicit destination wording so these operations are not confused with local persistence or JSON file import/export.
Suggested grouping:
The exact grouping may follow the current menu visual conventions, but keyboard order must match visual order.
This is a Workbench operation in the MVP. Do not expose it on detached/read-only Dashboard routes.
Save workspace flow
Opening Save shows a small dialog:
The name is editable metadata and defaults to the current workspace name. The note is optional.
Save sequence:
PortableBundleV1containing exactly one Dashboard and the complete workspace query catalog.INSERT ... FORMAT JSONEachRowrequest to the configured table.version,saved_at,saved_by, andpayload_hash.Reference shape:
Use the ordinary authenticated ClickHouse POST path with the SQL and JSONEachRow payload in the request body. Multi-megabyte workspace payloads are acceptable; do not introduce a separate upload protocol or URL-parameter transport.
Every explicit successful Save creates a new version, even when the payload matches the previous version.
payload_hashis diagnostic metadata, not an MVP deduplication gate.Save failure behavior
Search and Load flow
Open a dialog with a name/title search field:
Search returns up to ten latest Dashboard heads. Name is not an identity and duplicate names are allowed.
Each result shows enough metadata to distinguish duplicates:
The Dashboard ID may be visually muted or shown only when needed, but selection is always by stable
dashboard_id.Latest-head query
Use one tuple-valued
argMaxso all displayed metadata comes from the same latest row:The implementation may use an equivalent query, but it must not combine metadata from different versions.
Version list
After selecting a Dashboard, show its ten latest versions:
Example UI:
Ten versions is the MVP limit. Older versions remain in the table but do not need a Load-more UI in this issue.
Loading one version
Load by exact full version ID:
Before changing local state:
PortableBundleV1;nameas the local workspace name;No local workspace mutation occurs until the payload has completely decoded and validated.
Loading an older version does not update or delete server rows. A later Save appends a new version with a higher Snowflake ID under the same Dashboard ID.
Timestamp presentation
Snowflake time is authoritative. Derive it with
snowflakeIDToDateTime64(version).Store and query timestamps in UTC. Display them using the application's normal localized date/time formatting, while preserving the full instant in an accessible title or detail surface.
Security and privileges
The application relies on ClickHouse authorization.
Minimum required privileges on the configured table:
No UPDATE, DELETE, ALTER, or CREATE privilege is required.
Workspace bundles contain SQL and may contain sensitive literals. Deployments must choose the desired visibility model through ClickHouse grants and row policies, for example:
saved_by = currentUser();The application must not implement a second ACL system inside the payload.
Architecture
Introduce a dedicated server-workspace storage service behind an injected application/network seam.
Responsibilities should remain separated:
Do not make UI modules construct unescaped identifiers or raw payload SQL manually.
The local
WorkspaceRepositorycontract remains unchanged and authoritative for live editing.Cancellation and concurrency
Tests
Cover at least:
Configuration and schema
database.tableidentifier parsing and quoting;Save
Search/history
UInt64descending and limited to ten;Load
Permissions and errors
Acceptance criteria
config.jsoncan declare the exact ClickHouse table providing workspace storage for the current server/host.UInt64Snowflake IDs provide version identity and chronological ordering.currentUser()metadata are shown in search/history results.argMaxby Dashboard ID.