Status
Proposed architecture decision for V2.
This issue contains the proposed ADR and the implementation plan. When the validation phase passes, commit the reconciled decision as:
docs/ADR-0005-clickhouse-web-client.md
If a hard validation gate fails, do not force the migration. Record the evidence in the ADR, mark the decision Rejected, and retain the current transport with the layer separation from Phase 1.
Renumbered from ADR-0004 on 2026-08-03: that number is taken by the shipped docs/ADR-0004-ui-shell.md (vanilla shell decision).
Phases
Each phase ships as its own separate PR (owner decision, 2026-08-05) — this checklist is a convenience view; the <!-- ship-log --> comment on this issue is the state of record.
Per the ADR's own "Alternatives considered" section, Phase 1 remains valuable independent of the decision (separating application policy from the concrete transport implementation). The 2026-08-06 evidence run reached Rejected on two gates; a 2026-08-07 amendment reclassified both as non-blocking and moved the decision to Accepted. That same day, a Phase 2 implementation attempt went through 5 rounds of plan review (23 verified findings) before surfacing an eleventh, disqualifying consideration Phase 0 never measured: the official client's abort/cancellation model ties the real network request exclusively to its own internal controller, never to the caller's AbortSignal, which is structurally incompatible with the Phase 1 transport contract's requirement that the caller's own signal control cancellation for the whole response lifetime (including body streaming). Two independent architecture reviews (ChatGPT and a separate Fable/high reviewer) confirmed this independently and additionally found that, once every other required correction is applied (byte-exact SQL, byte-exact Authorization, a hand-written query-string serializer replacing the vendor's incompatible one), the official client contributes no bytes or behavior to the actual wire request. The decision reverted to Rejected the same day, 2026-08-07 — this is the new decision: Phases 2-4 (adoption, cutover, deletion) do not proceed without a new decision, and this reversion is that decision — it points away from adoption, not merely back to "re-evaluate later." Re-evaluation would need either an upstream client API that returns the native Response/rejection while leaving the caller's AbortSignal in control of the real fetch, or a deliberate renegotiation of the transport contract's cancellation semantics themselves.
Summary
Use the official @clickhouse/client-web package for generic ClickHouse HTTP transport beneath a small SQL Browser-owned adapter.
SQL Browser must continue to own its application policies:
- OAuth login and token refresh;
- credential-generation fencing and stale-request suppression;
- first-contact authentication denial versus query-level 401/403 classification;
- connection lifecycle state;
- idempotency-aware retry;
- per-tab ClickHouse session policy;
- explicit server-side cancellation;
- result caps and application result normalization;
- raw export and late-exception inspection;
- schema, documentation, lineage, dashboard, and capability queries.
The official client should own generic protocol mechanics where it provides behavioral parity:
- URL, setting, parameter, role, session, and query-id serialization;
- Basic/Bearer request headers;
- per-request auth and headers;
- timeout and
AbortSignal wiring;
- standard ClickHouse HTTP error parsing;
- response status, headers, and summary metadata;
- supported result parsing and stream handling;
- ordinary
query, exec, and command requests.
This is not a directive to replace src/net/ch-client.ts mechanically. The migration is accepted only after the spike proves precision, compatibility, streaming, authentication, cancellation, export, and bundle behavior.
Problem
src/net/ch-client.ts currently combines three different responsibilities:
- generic ClickHouse HTTP transport;
- SQL Browser authentication, lifecycle, retry, and cancellation policy;
- product-specific schema, documentation, lineage, dashboard, and capability operations.
The custom code was reasonable while the browser client did not satisfy the application's requirements. The current official web client now supports the browser primitives SQL Browser needs, including injected fetch, Web Streams, Basic and Bearer/JWT auth, per-request auth overrides, settings, query parameters, HTTP headers, sessions, query IDs, roles, abort signals, raw execution, progress-bearing JSON formats, response headers, and status metadata.
The previous rejection recorded in PR #95 included three arguments:
- the official client could not control browser sockets and therefore could not solve the HTTP-session lock/reset problem;
- auth was considered static at client construction and incompatible with the OAuth refresh seam;
- it would add another runtime dependency.
The first point remains true but is not a client-selection criterion: neither a custom fetch wrapper nor the official web client controls browser TCP connection affinity. The application already solved that problem through selective logical-session use and idempotency-aware retry.
The second point is no longer true for the current official client: it supports per-request auth overrides. SQL Browser still owns token acquisition and refresh, but it can pass the current credential to each official-client request.
The third point remains a real tradeoff and must be measured against the code and protocol-maintenance burden removed.
Goals
- Make the production transport boundary smaller, clearer, and based on the maintained official ClickHouse web client.
- Preserve all current SQL Browser behavior and race-safety invariants.
- Keep authentication and product policy independent of the third-party client.
- Remove application-owned generic protocol code after cutover.
- Retain the single self-contained HTML distribution.
- Create a transport boundary that can support future binary formats without decoding bytes through text.
- Make the decision empirical and reversible until the validation gates pass.
Non-goals
- Rewriting OAuth, connection lifecycle, query execution, workbench, Dashboard, or catalogue services around the official client's object model.
- Moving application policy into
@clickhouse/client-web wrappers or callbacks.
- Replacing SQL Browser result models with official-client result objects outside
src/net/.
- Changing SQL behavior, parameter syntax, result caps, retry policy, session semantics, or user-facing errors.
- Adopting an ORM, query builder, framework integration, or Node-only ClickHouse client.
- Adding a second permanent transport path.
- Performing unrelated V2 UI redesign in the same PRs.
- Contributing a broad redesign to
ClickHouse/clickhouse-js; upstream only the narrow missing capability required by this migration.
Proposed ADR-0005
Title
Adopt @clickhouse/client-web beneath a SQL Browser-owned transport adapter.
Status
Proposed.
Change to Accepted only after Phase 0 passes every hard gate. Change to Rejected if a hard gate cannot be satisfied without maintaining two generic clients or materially weakening current behavior.
Context
SQL Browser is a strict-TypeScript browser SPA built into one self-contained HTML file. It communicates directly with ClickHouse over HTTP and supports OAuth and Basic authentication, progressive query results, cancellation, native query parameters, raw export, per-tab logical sessions, schema browsing, lineage, documentation lookup, Workbench execution, and Dashboard execution.
The repository's current custom client provides both low-level protocol mechanics and high-level application behavior. This increases the amount of code SQL Browser must maintain when ClickHouse HTTP behavior, formats, error signaling, parameters, compression, headers, or browser stream handling change.
The official ClickHouse web client is now sufficiently capable to be evaluated as the generic transport layer. It does not, and should not, own SQL Browser's OAuth/session lifecycle or product-specific policies.
Decision
Subject to the validation gates below:
- Add
@clickhouse/client-web as the only generic ClickHouse HTTP client dependency.
- Keep a SQL Browser-owned transport interface under
src/net/.
- Do not expose
@clickhouse/client-web types, result sets, errors, or configuration objects above the network layer.
- Resolve the current credential before each request and pass it through the official client's per-request auth override.
- Keep token refresh, credential epochs, stale-response fencing, auth classification, retry, session activation, remote cancellation, connection-state reporting, caps, and result normalization in SQL Browser.
- Route SQL without an authored output clause through the appropriate official
query API only when the client can represent the required format precisely.
- Route complete authored SQL, arbitrary
FORMAT, raw output, and binary-capable paths through exec or a narrow raw transport method.
- Keep schema/catalogue/lineage/documentation functions as SQL Browser domain operations that depend on the transport interface; do not move them into a generic driver wrapper.
- Delete superseded generic request construction, standard error parsing, supported stream parsing, and duplicated serialization code after the cutover.
- Do not retain a runtime switch between custom and official clients after migration.
Target architecture
UI / route sessions
|
application services
- ConnectionSession
- QueryExecutionService
- SchemaCatalogService
- ExportService
- DashboardViewerSession
|
SQL Browser ClickHouse adapter
- auth/epoch policy integration
- request classification
- SQL Browser result normalization
- raw/late-error policy
|
@clickhouse/client-web
|
Fetch / Web Streams / ClickHouse HTTP API
Expected source shape; exact names may follow repository conventions:
src/net/clickhouse-transport.ts
src/net/clickhouse-web-transport.ts
src/net/ch-client.ts
ch-client.ts may remain the product-operation module, but generic transport must be delegated through the narrow transport interface rather than reconstructed inline.
Ownership boundary
Official client owns
- HTTP URL construction for supported request fields;
- ClickHouse settings serialization;
- native query-parameter serialization, including multipart support when selected;
- database, role,
session_id, and query_id request fields;
- Basic/Bearer header construction from the credential passed for that request;
- standard request timeout and abort plumbing;
- standard ClickHouse error detection and parsing;
- supported JSON row parsing and stream mechanics;
- response headers, status, and summary metadata;
- ordinary command execution.
SQL Browser owns
- OAuth discovery, PKCE, login, token storage, refresh, and logout;
- mutable connection origin and auth mode;
- the authoritative credential epoch;
- rechecking the epoch after every credential await and immediately before every side effect;
- one-refresh retry and refresh single-flight behavior;
- distinguishing login rejection from a query-level 401/403 after authentication has been confirmed;
- connection state (
starting, connected, refreshing, offline, auth-required, reauthenticating, signed-out);
- application retry classification, including idempotency and
SESSION_IS_LOCKED;
- per-tab logical-session activation and stickiness;
- owner-scoped
AbortController lifecycle;
- explicit
KILL QUERY using the exact frozen credential lease during scope teardown;
- row caps, result model, progress counters, statistics, cancellation state, and UI normalization;
- authored-format classification;
- raw export, exact-byte output, partial-file rules, and late exception detection;
- all schema, catalogue, documentation, lineage, Dashboard, and capability SQL.
Hard invariants
The migration must preserve all of these:
- No stale credential use. A request started in one credential epoch must never send a token or mutate lifecycle state after that epoch has been replaced.
- No refresh authority leak. A stale request may not initiate or complete refresh for a replacement session.
- One refresh retry. Authentication retry behavior stays bounded and observable exactly as today.
- Correct 401/403 classification. A post-confirmation ClickHouse permission/query error must not sign the user out.
- Transport-state correctness. Only a successful current 2xx request reports connected; rejected non-aborted network I/O reports offline; HTTP query errors remain query outcomes.
- Owner-scoped cancellation. Route/session owners retain their current abort lifecycle; the transport does not become the owner of application cancellation state.
- Server cancellation. Query IDs remain available before execution, and Cancel continues to abort locally and issue best-effort
KILL QUERY remotely.
- Frozen cancellation lease. Authentication-scope teardown does not read mutable tokens, refresh, or auth mode.
- Progressive results. Table/KPI first rows are not delayed until query completion.
- Mid-stream errors. Exceptions delivered after HTTP headers are surfaced as query errors and never silently converted into successful partial results.
- Exact values.
UInt64, 128/256-bit integers, decimals, UUIDs, dates, times, arrays, tuples, maps, nullable values, and strings must retain the current wire-level precision and representation expected by result normalization.
- Native parameters. Existing
{name:Type} binding, URL/multipart behavior, large values, arrays, and exact integer strings remain injection-safe and semantically identical.
- Logical sessions. Temporary tables and session
SET behavior stay per-tab and ordinary queries remain session-less unless the tab has activated a session.
- Retry safety. Connection-reset retry remains limited to statements classified safe to retry; ambiguous DDL/INSERT outcomes are never automatically repeated.
- Auth modes. OAuth Bearer, JWT-as-Basic-password, ordinary Basic username/password, same-origin, and configured cross-origin hosts continue to work.
- Arbitrary formats. User-authored
FORMAT clauses, implicit raw formats, EXPLAIN output, and exports must not receive a duplicate appended format.
- Exact raw bytes. Export and future binary result paths must not pass through
Response.text() or an equivalent UTF-8 decode.
- Fallback behavior. Existing old-server and capability fallbacks continue to be classified by the application, including DataLakeCatalog visibility and missing/denied system tables.
- Single artifact. The self-contained
dist/sql.html distribution and CSP/deployment model remain supported.
- No dual ownership. After cutover, exactly one generic transport implementation exists in production.
Known gap: JSONStringsEachRowWithProgress
Normal table execution currently uses JSONStringsEachRowWithProgress to preserve string representations for large integers, decimals, and related ClickHouse values while receiving progress events.
The current official client's declared supported format set includes JSONEachRowWithProgress but not JSONStringsEachRowWithProgress.
This is the main migration blocker and must be resolved explicitly. Acceptable outcomes:
- add and upstream official-client support for
JSONStringsEachRowWithProgress, then consume the released version;
- use official
exec() only for the request and retain one small SQL Browser parser for this exact line-oriented format;
- prove with parity tests that another official-client path preserves every required value exactly.
A broad duplicate result parser is not acceptable. If option 2 is chosen, the retained parser must be narrowly scoped, documented as an unsupported-format bridge, and removed when upstream support is available.
Do not switch normal tables to JSONEachRowWithProgress merely because the TypeScript API accepts it. Precision tests must prove equivalence first.
Auth integration
Do not construct a new official client for every token refresh.
The SQL Browser adapter must:
- capture the current credential epoch before its first await;
- obtain the current credential through
ConnectionSession;
- reject as cancellation if the epoch changed;
- pass the complete current credential through the official client's per-request auth override;
- recheck epoch authority before the request side effect where the adapter can do so;
- classify the returned error using existing SQL Browser rules;
- run at most one application-owned refresh retry;
- never let official-client defaults become the authority for current credentials.
The client-level config may contain a non-secret placeholder/default credential only if the official constructor requires one. Every authenticated production request must use the explicit request credential supplied by SQL Browser.
Query API routing
The adapter, not application callers, chooses the official API:
query() for supported result formats where the adapter owns the appended format;
exec() for complete SQL, authored FORMAT, raw response, unsupported formats, and binary-capable paths;
command() for no-output commands when discarding the response is correct;
- a SQL Browser operation for
KILL QUERY, preserving the frozen-lease path.
No service above src/net/ should decide between official-client methods.
Consequences
Positive
- Less application-owned generic protocol code.
- Maintained ClickHouse parameter, settings, error, stream, and header behavior.
- Easier adoption of official features and fixes.
- Clearer separation between transport and SQL Browser policy.
- Better interoperability with ClickHouse documentation and examples.
- Reduced risk of silently skipping malformed protocol lines or missing new error signaling.
Negative
- Added bundled code and dependency-update responsibility.
- The adapter must translate official errors and results into existing SQL Browser contracts.
- Some SQL Browser formats or raw behaviors may still need narrow bridges.
- Browser and server-version compatibility must be tested rather than assumed.
- A migration can introduce subtle regressions in authentication races, streaming, or precision even when basic queries pass.
Neutral
The official client does not solve browser connection affinity, OAuth, product retry policy, schema discovery, or UI result management. Those remain application concerns.
Alternatives considered
Keep the current combined client unchanged
Lowest short-term change risk, but retains mixed responsibilities and long-term protocol maintenance. Rejected as the V2 target unless the official-client validation fails.
Replace all network and execution code with official-client objects
Would leak driver concerns into services and discard proven SQL Browser policy. Rejected.
Use another community JS client
The maintained official web package is the only material browser-targeted candidate. Most community packages are Node-only, older, less complete, or query-builder oriented. Rejected.
Call fetch directly behind a newly separated custom transport
A valid fallback if the official package fails the hard gates. Phase 1 is still valuable in that outcome, but the ADR must be marked Rejected and explain why custom generic transport remains necessary.
Delivery plan
Phase 0 / PR 1 — validation spike and ADR evidence
No production cutover in this PR.
Dependency and build
- Add the official web client on a spike branch.
- Produce the normal self-contained build.
- Record before/after:
- unminified JS bytes;
- minified JS bytes;
- final
dist/sql.html bytes;
- compressed artifact size using the repository's normal reporting method;
- startup parse/evaluation measurement if an existing budget or harness exists.
- Confirm no CDN or runtime network import is introduced.
- Confirm CSP and local/deployed modes still permit the bundle.
Compatibility matrix
Test the official path against every ClickHouse version currently promised by repository documentation or deployment policy.
If no explicit support matrix exists, the PR must identify the oldest version exercised by CI/demo/deployment and propose a documented minimum. The ADR cannot be accepted based only on the newest server.
At minimum cover:
- oldest supported OSS/Altinity ClickHouse;
- current stable ClickHouse;
- ClickHouse Cloud where credentials are available;
- same-origin deployment;
- cross-origin local mode with CORS;
- Chromium and WebKit.
Parity harness
Build a reusable parity suite that can run the same request through:
current custom transport
official-client spike transport
Compare normalized outcomes, not internal object identity.
Cover:
- ordinary JSON query;
- table streaming with progress;
- KPI streaming;
- empty result;
- totals/extremes/rows-before-limit events where supported;
- server error before headers;
- exception after headers/in stream;
- malformed/truncated stream;
- cancellation before request, during headers, and during row streaming;
- request timeout;
- response headers and query ID;
X-ClickHouse-Summary;
- query settings;
- role;
- logical session;
- query parameters in URL;
- forced multipart parameters;
- automatic multipart promotion for a large value;
- explicit
FORMAT SQL;
- raw TSV/CSV/JSON;
- command with no useful response;
- exact export stream;
- Basic auth;
- Bearer auth;
- per-request auth replacement;
- failed first credential, refresh, successful retry;
- post-confirmation 401/403 query error;
- stale credential epoch before request;
- stale epoch during refresh;
- stale response after a replacement session;
- offline fetch rejection versus HTTP query error.
Precision corpus
The parity suite must include values that fail if coerced through JavaScript number or normalized differently:
UInt64 max and values above Number.MAX_SAFE_INTEGER
Int64 min/max
UInt128 / UInt256
Int128 / Int256
Decimal32/64/128/256 with scale
Date / Date32
DateTime with timezone
DateTime64 with fractional precision and timezone
UUID
IPv4 / IPv6
Enum8 / Enum16
Nullable values
Array of large integers and dates
Tuple with named and unnamed elements
Map
LowCardinality
JSON/Object values where supported
strings containing newlines, NUL, Unicode, backslashes, quotes, tabs
Assertions must compare the exact values consumed by SQL Browser result normalization.
Critical questions the spike must answer
- Can the official client request or safely expose
JSONStringsEachRowWithProgress?
- If not, how many lines of narrow bridge code are required?
- Does
exec() expose the raw byte stream needed by exports and future binary formats without text decoding?
- Can the adapter preserve current mid-stream exception behavior?
- Can per-request auth be supplied without mutating or reconstructing the client?
- Can epoch fencing happen immediately before the real fetch side effect, or is an injected fetch guard required?
- How are abort, timeout, and ClickHouse errors distinguished?
- Does official error parsing retain the ClickHouse code and message required by existing retry/auth classification?
- Does the client support the oldest ClickHouse version SQL Browser promises?
- What production code is deleted at final cutover?
Phase 0 output
Update this issue or the PR description with a decision table:
| Gate |
Result |
Evidence |
| exact-value parity |
pass/fail |
test/fixture |
| progressive first-row parity |
pass/fail |
measurement |
| mid-stream error parity |
pass/fail |
test |
| auth/epoch parity |
pass/fail |
race tests |
| raw/export bytes |
pass/fail |
hash comparison |
| supported-server matrix |
pass/fail |
versions |
| browser matrix |
pass/fail |
Chromium/WebKit |
| single-file build |
pass/fail |
artifact |
| bundle delta |
measured |
bytes |
| net production-code deletion |
estimated |
LOC/modules |
Then reconcile the ADR status:
- Accepted: every hard behavioral gate passes and the final plan deletes superseded generic production code.
- Rejected: any hard gate cannot be met without permanent dual clients, precision loss, weakened auth/cancellation guarantees, unsupported required servers, or text decoding of raw bytes.
Phase 1 / PR 2 — establish the transport seam without behavior change
This phase is valuable whether ADR-0005 is accepted or rejected.
- Define a narrow SQL Browser transport contract under
src/net/.
- Put the current implementation behind it first.
- Preserve all public application/service signatures where practical.
- Move no product SQL into the generic transport.
- Keep
ChCtx/connection seams narrow and testable.
- Add architecture checks preventing imports of
@clickhouse/client-web outside the official transport implementation and its tests.
- Add contract tests shared by current and official implementations.
- No user-visible behavior change.
Suggested conceptual contract; adapt to established repository types rather than copying verbatim:
interface ClickHouseTransport {
query<T>(request: StructuredQueryRequest): Promise<StructuredQueryResult<T>>;
stream(request: StreamQueryRequest): Promise<StreamQueryHandle>;
exec(request: RawQueryRequest): Promise<RawQueryHandle>;
command(request: CommandRequest): Promise<CommandResult>;
}
The contract must carry SQL Browser needs without exposing official-client classes:
- SQL;
- format intent;
- settings;
- native query parameters;
- role/session/query ID;
- complete per-request credential;
AbortSignal;
- status/headers/summary;
- raw byte stream where required.
Authentication refresh is not a transport-interface method.
Phase 2 / PR 3 — official transport implementation
- Implement the transport contract with
@clickhouse/client-web.
- Reuse one client per connection origin/configuration where safe; do not make token lifetime client lifetime.
- Pass credentials per request.
- Use injected
fetch so existing tests and epoch guards remain possible.
- Translate official errors once at the network boundary into the existing SQL Browser error taxonomy/data.
- Implement the chosen
JSONStringsEachRowWithProgress solution.
- Keep product callers on the same SQL Browser transport contract.
- Run both implementations only in tests/parity harnesses; production still uses the current implementation until the cutover PR.
Phase 3 / PR 4 — production cutover
Cut over in bounded slices if needed, but each merged slice must have one owner for each request category.
Recommended order:
queryJson-style metadata and catalogue reads;
- ordinary no-output commands;
- KPI streaming;
- table streaming;
- raw authored formats;
- export/raw-byte streaming;
- cancellation/KILL integration.
For each slice:
- preserve existing application-level API and result shape;
- run shared contract tests;
- add real-browser coverage where Fetch/Web Streams behavior matters;
- delete the superseded current implementation for that slice in the same PR or immediately following cutover PR.
No permanent feature flag or user preference may select the client.
Phase 4 / PR 5 — delete generic custom transport and reconcile architecture
- Delete superseded manual URL construction covered by the official client.
- Delete duplicated standard parameter/settings/auth/header serialization.
- Delete duplicated standard error parsing where the adapter translation fully replaces it.
- Delete duplicated supported-format stream parsing.
- Retain only narrow bridges explicitly approved by the ADR.
- Keep schema/catalogue/lineage/documentation operations, but make them consume the transport seam.
- Split
src/net/ch-client.ts if the remaining domain operations are still too broad; do not create one replacement god module.
- Remove spike switches, duplicate fixtures, temporary compatibility paths, and migration-only comments.
- Update
docs/ARCHITECTURE.md, dependency documentation, CHANGELOG.md, and the accepted ADR.
- Record final production LOC and bundle delta against the Phase 0 baseline.
Upstream work
If the only blocker is official support for JSONStringsEachRowWithProgress or another narrow format/export need:
- open a focused issue or PR in
ClickHouse/clickhouse-js;
- include an integration test and type-level format support;
- avoid SQL Browser-specific lifecycle policy upstream;
- do not merge the SQL Browser production cutover until the dependency version containing the required fix is available, unless the ADR explicitly accepts a narrow temporary bridge.
Tests
Unit and contract
- transport request mapping for settings, parameters, role, session, query ID, headers, and auth;
- per-request auth replaces client defaults;
- exact error translation including ClickHouse error code/message;
- epoch checks before credential use and fetch side effect;
- one-refresh retry and no second retry;
- post-confirmation 401/403 remains a query outcome;
- abort and timeout remain distinguishable;
- response status/headers/summary preserved;
- authored
FORMAT never receives a second format;
- command drains/discards only where intended;
- raw handles expose bytes/stream, never forced text;
- contract suite runs against both implementations before cutover and official only after cleanup.
Integration
- precision corpus against real ClickHouse;
- progress and first-row timing;
- mid-stream exception;
- query cancellation plus
KILL QUERY observed server-side where test infrastructure permits;
- temporary table and session
SET across statements;
- safe retry on
SESSION_IS_LOCKED/read reset;
- no retry for ambiguous INSERT/DDL reset;
- same-origin and cross-origin auth;
- OAuth refresh simulation with epoch replacement;
- raw export byte hash equality.
Browser E2E
Chromium and WebKit:
- login/connect and ordinary query;
- long progressive query, visible progress, Cancel;
- sign-out/auth-loss during a running query;
- sign in as a replacement user while old work settles;
- Workbench table/KPI/raw results;
- Dashboard concurrent tile execution and refresh;
- export cancellation and successful download/write path;
- explicit format result;
- no unhandled promise rejection or leaked stream on route teardown.
Build and architecture
npm test;
npm run check:arch;
npm run check:types;
npm run build;
- relevant Chromium/WebKit suites;
- one self-contained artifact;
- no direct import of
@clickhouse/client-web outside src/net/clickhouse-web-transport.ts (or the final named equivalent) and its tests;
- no obsolete custom generic client after cutover.
Acceptance criteria
Decision gate
Architecture
Behavior
Simplification
Definition of done
V2 uses one maintained official ClickHouse web client for generic HTTP mechanics behind a SQL Browser-owned adapter, while every application-specific authentication, lifecycle, safety, streaming, session, cancellation, export, and product-query invariant remains intact. The previous generic custom implementation is removed except for any narrowly documented bridge that the accepted ADR explicitly permits.
If the validation cannot satisfy that definition without weakening behavior or keeping two generic clients, the ADR is marked Rejected and the repository keeps the separated custom transport produced by Phase 1.
Status
Proposed architecture decision for V2.
This issue contains the proposed ADR and the implementation plan. When the validation phase passes, commit the reconciled decision as:
If a hard validation gate fails, do not force the migration. Record the evidence in the ADR, mark the decision Rejected, and retain the current transport with the layer separation from Phase 1.
Renumbered from ADR-0004 on 2026-08-03: that number is taken by the shipped
docs/ADR-0004-ui-shell.md(vanilla shell decision).Phases
Each phase ships as its own separate PR (owner decision, 2026-08-05) — this checklist is a convenience view; the
<!-- ship-log -->comment on this issue is the state of record.docs/ADR-0005-clickhouse-web-client.md; briefly amended to Accepted 2026-08-07, then reverted to Rejected the same day after Phase 2's attempt — see the ADR's "Phase 2 cancellation-incompatibility addendum")Per the ADR's own "Alternatives considered" section, Phase 1 remains valuable independent of the decision (separating application policy from the concrete transport implementation). The 2026-08-06 evidence run reached Rejected on two gates; a 2026-08-07 amendment reclassified both as non-blocking and moved the decision to Accepted. That same day, a Phase 2 implementation attempt went through 5 rounds of plan review (23 verified findings) before surfacing an eleventh, disqualifying consideration Phase 0 never measured: the official client's abort/cancellation model ties the real network request exclusively to its own internal controller, never to the caller's
AbortSignal, which is structurally incompatible with the Phase 1 transport contract's requirement that the caller's own signal control cancellation for the whole response lifetime (including body streaming). Two independent architecture reviews (ChatGPT and a separate Fable/high reviewer) confirmed this independently and additionally found that, once every other required correction is applied (byte-exact SQL, byte-exact Authorization, a hand-written query-string serializer replacing the vendor's incompatible one), the official client contributes no bytes or behavior to the actual wire request. The decision reverted to Rejected the same day, 2026-08-07 — this is the new decision: Phases 2-4 (adoption, cutover, deletion) do not proceed without a new decision, and this reversion is that decision — it points away from adoption, not merely back to "re-evaluate later." Re-evaluation would need either an upstream client API that returns the nativeResponse/rejection while leaving the caller'sAbortSignalin control of the real fetch, or a deliberate renegotiation of the transport contract's cancellation semantics themselves.Summary
Use the official
@clickhouse/client-webpackage for generic ClickHouse HTTP transport beneath a small SQL Browser-owned adapter.SQL Browser must continue to own its application policies:
The official client should own generic protocol mechanics where it provides behavioral parity:
AbortSignalwiring;query,exec, andcommandrequests.This is not a directive to replace
src/net/ch-client.tsmechanically. The migration is accepted only after the spike proves precision, compatibility, streaming, authentication, cancellation, export, and bundle behavior.Problem
src/net/ch-client.tscurrently combines three different responsibilities:The custom code was reasonable while the browser client did not satisfy the application's requirements. The current official web client now supports the browser primitives SQL Browser needs, including injected
fetch, Web Streams, Basic and Bearer/JWT auth, per-request auth overrides, settings, query parameters, HTTP headers, sessions, query IDs, roles, abort signals, raw execution, progress-bearing JSON formats, response headers, and status metadata.The previous rejection recorded in PR #95 included three arguments:
The first point remains true but is not a client-selection criterion: neither a custom
fetchwrapper nor the official web client controls browser TCP connection affinity. The application already solved that problem through selective logical-session use and idempotency-aware retry.The second point is no longer true for the current official client: it supports per-request auth overrides. SQL Browser still owns token acquisition and refresh, but it can pass the current credential to each official-client request.
The third point remains a real tradeoff and must be measured against the code and protocol-maintenance burden removed.
Goals
Non-goals
@clickhouse/client-webwrappers or callbacks.src/net/.ClickHouse/clickhouse-js; upstream only the narrow missing capability required by this migration.Proposed ADR-0005
Title
Adopt
@clickhouse/client-webbeneath a SQL Browser-owned transport adapter.Status
Proposed.
Change to Accepted only after Phase 0 passes every hard gate. Change to Rejected if a hard gate cannot be satisfied without maintaining two generic clients or materially weakening current behavior.
Context
SQL Browser is a strict-TypeScript browser SPA built into one self-contained HTML file. It communicates directly with ClickHouse over HTTP and supports OAuth and Basic authentication, progressive query results, cancellation, native query parameters, raw export, per-tab logical sessions, schema browsing, lineage, documentation lookup, Workbench execution, and Dashboard execution.
The repository's current custom client provides both low-level protocol mechanics and high-level application behavior. This increases the amount of code SQL Browser must maintain when ClickHouse HTTP behavior, formats, error signaling, parameters, compression, headers, or browser stream handling change.
The official ClickHouse web client is now sufficiently capable to be evaluated as the generic transport layer. It does not, and should not, own SQL Browser's OAuth/session lifecycle or product-specific policies.
Decision
Subject to the validation gates below:
@clickhouse/client-webas the only generic ClickHouse HTTP client dependency.src/net/.@clickhouse/client-webtypes, result sets, errors, or configuration objects above the network layer.queryAPI only when the client can represent the required format precisely.FORMAT, raw output, and binary-capable paths throughexecor a narrow raw transport method.Target architecture
Expected source shape; exact names may follow repository conventions:
ch-client.tsmay remain the product-operation module, but generic transport must be delegated through the narrow transport interface rather than reconstructed inline.Ownership boundary
Official client owns
session_id, andquery_idrequest fields;SQL Browser owns
starting,connected,refreshing,offline,auth-required,reauthenticating,signed-out);SESSION_IS_LOCKED;AbortControllerlifecycle;KILL QUERYusing the exact frozen credential lease during scope teardown;Hard invariants
The migration must preserve all of these:
KILL QUERYremotely.UInt64, 128/256-bit integers, decimals, UUIDs, dates, times, arrays, tuples, maps, nullable values, and strings must retain the current wire-level precision and representation expected by result normalization.{name:Type}binding, URL/multipart behavior, large values, arrays, and exact integer strings remain injection-safe and semantically identical.SETbehavior stay per-tab and ordinary queries remain session-less unless the tab has activated a session.FORMATclauses, implicit raw formats, EXPLAIN output, and exports must not receive a duplicate appended format.Response.text()or an equivalent UTF-8 decode.dist/sql.htmldistribution and CSP/deployment model remain supported.Known gap:
JSONStringsEachRowWithProgressNormal table execution currently uses
JSONStringsEachRowWithProgressto preserve string representations for large integers, decimals, and related ClickHouse values while receiving progress events.The current official client's declared supported format set includes
JSONEachRowWithProgressbut notJSONStringsEachRowWithProgress.This is the main migration blocker and must be resolved explicitly. Acceptable outcomes:
JSONStringsEachRowWithProgress, then consume the released version;exec()only for the request and retain one small SQL Browser parser for this exact line-oriented format;A broad duplicate result parser is not acceptable. If option 2 is chosen, the retained parser must be narrowly scoped, documented as an unsupported-format bridge, and removed when upstream support is available.
Do not switch normal tables to
JSONEachRowWithProgressmerely because the TypeScript API accepts it. Precision tests must prove equivalence first.Auth integration
Do not construct a new official client for every token refresh.
The SQL Browser adapter must:
ConnectionSession;The client-level config may contain a non-secret placeholder/default credential only if the official constructor requires one. Every authenticated production request must use the explicit request credential supplied by SQL Browser.
Query API routing
The adapter, not application callers, chooses the official API:
query()for supported result formats where the adapter owns the appended format;exec()for complete SQL, authoredFORMAT, raw response, unsupported formats, and binary-capable paths;command()for no-output commands when discarding the response is correct;KILL QUERY, preserving the frozen-lease path.No service above
src/net/should decide between official-client methods.Consequences
Positive
Negative
Neutral
The official client does not solve browser connection affinity, OAuth, product retry policy, schema discovery, or UI result management. Those remain application concerns.
Alternatives considered
Keep the current combined client unchanged
Lowest short-term change risk, but retains mixed responsibilities and long-term protocol maintenance. Rejected as the V2 target unless the official-client validation fails.
Replace all network and execution code with official-client objects
Would leak driver concerns into services and discard proven SQL Browser policy. Rejected.
Use another community JS client
The maintained official web package is the only material browser-targeted candidate. Most community packages are Node-only, older, less complete, or query-builder oriented. Rejected.
Call
fetchdirectly behind a newly separated custom transportA valid fallback if the official package fails the hard gates. Phase 1 is still valuable in that outcome, but the ADR must be marked Rejected and explain why custom generic transport remains necessary.
Delivery plan
Phase 0 / PR 1 — validation spike and ADR evidence
No production cutover in this PR.
Dependency and build
dist/sql.htmlbytes;Compatibility matrix
Test the official path against every ClickHouse version currently promised by repository documentation or deployment policy.
If no explicit support matrix exists, the PR must identify the oldest version exercised by CI/demo/deployment and propose a documented minimum. The ADR cannot be accepted based only on the newest server.
At minimum cover:
Parity harness
Build a reusable parity suite that can run the same request through:
Compare normalized outcomes, not internal object identity.
Cover:
X-ClickHouse-Summary;FORMATSQL;Precision corpus
The parity suite must include values that fail if coerced through JavaScript
numberor normalized differently:Assertions must compare the exact values consumed by SQL Browser result normalization.
Critical questions the spike must answer
JSONStringsEachRowWithProgress?exec()expose the raw byte stream needed by exports and future binary formats without text decoding?Phase 0 output
Update this issue or the PR description with a decision table:
Then reconcile the ADR status:
Phase 1 / PR 2 — establish the transport seam without behavior change
This phase is valuable whether ADR-0005 is accepted or rejected.
src/net/.ChCtx/connection seams narrow and testable.@clickhouse/client-weboutside the official transport implementation and its tests.Suggested conceptual contract; adapt to established repository types rather than copying verbatim:
The contract must carry SQL Browser needs without exposing official-client classes:
AbortSignal;Authentication refresh is not a transport-interface method.
Phase 2 / PR 3 — official transport implementation
@clickhouse/client-web.fetchso existing tests and epoch guards remain possible.JSONStringsEachRowWithProgresssolution.Phase 3 / PR 4 — production cutover
Cut over in bounded slices if needed, but each merged slice must have one owner for each request category.
Recommended order:
queryJson-style metadata and catalogue reads;For each slice:
No permanent feature flag or user preference may select the client.
Phase 4 / PR 5 — delete generic custom transport and reconcile architecture
src/net/ch-client.tsif the remaining domain operations are still too broad; do not create one replacement god module.docs/ARCHITECTURE.md, dependency documentation,CHANGELOG.md, and the accepted ADR.Upstream work
If the only blocker is official support for
JSONStringsEachRowWithProgressor another narrow format/export need:ClickHouse/clickhouse-js;Tests
Unit and contract
FORMATnever receives a second format;Integration
KILL QUERYobserved server-side where test infrastructure permits;SETacross statements;SESSION_IS_LOCKED/read reset;Browser E2E
Chromium and WebKit:
Build and architecture
npm test;npm run check:arch;npm run check:types;npm run build;@clickhouse/client-weboutsidesrc/net/clickhouse-web-transport.ts(or the final named equivalent) and its tests;Acceptance criteria
Decision gate
JSONStringsEachRowWithProgressstrategy is decided and tested.Architecture
src/net/.@clickhouse/client-web.Behavior
Simplification
docs/ADR-0005-clickhouse-web-client.md,docs/ARCHITECTURE.md, dependency docs, andCHANGELOG.mdare updated.Definition of done
V2 uses one maintained official ClickHouse web client for generic HTTP mechanics behind a SQL Browser-owned adapter, while every application-specific authentication, lifecycle, safety, streaming, session, cancellation, export, and product-query invariant remains intact. The previous generic custom implementation is removed except for any narrowly documented bridge that the accepted ADR explicitly permits.
If the validation cannot satisfy that definition without weakening behavior or keeping two generic clients, the ADR is marked Rejected and the repository keeps the separated custom transport produced by Phase 1.