Skip to content

feat: run locally + host picker from clickhouse-client (basic + OAuth)#52

Merged
BorisTyshkevich merged 7 commits into
mainfrom
feat/local-runner
Jun 27, 2026
Merged

feat: run locally + host picker from clickhouse-client (basic + OAuth)#52
BorisTyshkevich merged 7 commits into
mainfrom
feat/local-runner

Conversation

@BorisTyshkevich

@BorisTyshkevich BorisTyshkevich commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

npm run local builds the SPA, serves it on localhost, and reads your ~/.clickhouse-client/config.xml connections into a Saved connection dropdown on the login screen — so you pick a cluster instead of typing it.

npm run local      # build + serve → open http://localhost:8900/sql
  • A plain connection (hostname/user/password) → prefills the credentials form (cross-origin HTTP Basic).
  • A connection with clickhouse-client's OAuth keys (oauth-url/oauth-client-id/oauth-audience) → an OAuth sign-in against that cluster.
  • You can still type a host/user/password by hand.

How it works (no ClickHouse here)

The app is a thin client — queries go straight from the browser to the chosen cluster. The local server only serves the page + a generated config.json. No clickhouse server is spawned (its macOS build has a flaky setClustersConfig startup crash; see history below), and nothing is proxied.

Changes

SPA (shipped, config-driven, tested):

  • oauth-config.js: optional hosts[] in config.json → { idps, basicLogin, hosts } with normalizeHost.
  • app.js: OAuth can target a cross-origin cluster — login(idpId, targetOrigin) stashes oauth_origin (survives the redirect like oauth_idp), chCtx.origin reads it in OAuth mode, sign-out clears it; redirect_uri stays the localhost SPA.
  • login.js + styles.css: the host <select> (basic → prefill; oauth → SSO against that cluster).

Local glue:

  • build/local.py: a stdlib server that parses connections_credentialshosts[] (basic creds autofilled; oauth-* keys → oauth host + IdP, bearer=access_token when an audience is set) and serves the generated config. PORT / LOCAL_CH_CONFIG overridable.
  • README "Run locally" section.

Prerequisites for OAuth connections (one-time)

  • Register http://localhost:8900/sql as a redirect URI with the IdP.
  • Allow CORS from http://localhost:8900 on the cluster (ClickHouse sends Access-Control-Allow-Origin for Origin-bearing requests by default).

946 tests pass; per-file coverage gate green. Verified the generated config against a real 22-connection config (21 basic + 1 OAuth) without leaking secrets, and that the server serves /sql + /sql/config.json.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u

BorisTyshkevich and others added 2 commits June 26, 2026 17:55
Add a one-command way for anyone cloning the repo to run the whole app
locally without a cluster or Docker: `npm run local` boots an
already-installed `clickhouse` binary as a standalone server in a gitignored
.local-ch/ dir and serves the built SPA same-origin from its user_files (the
same static-handler trick as the production deploy), basic-auth, no IdP. The
browser POSTs queries to "/", so the schema tree, results, charts, and the
lineage graph all work offline.

- build/local.mjs: dependency-free runner — finds the clickhouse binary
  (`clickhouse server` or `clickhouse-server`), scaffolds config.xml/users.xml,
  stages dist/sql.html + a {basic_login:true} config.json, boots the server,
  prints the URL, forwards Ctrl-C. Data dir persists across runs; env overrides
  LOCAL_CH_DIR / CH_HTTP_PORT (default 18123) / CH_TCP_PORT.
- Bakes in the one non-obvious gotcha: CH 26.x null-pointer-crashes at startup
  if <remote_servers> is missing/empty (Context::setClustersConfig), so the
  generated config includes one concrete cluster.
- package.json: `local` script; .gitignore: /.local-ch/; README: a "Run locally
  (no Docker)" section under Quick start.

No src/ changes — the coverage gate is unaffected (935 tests pass). Verified
end-to-end: serves /sql (200), /sql/config.json, and a CREATE/INSERT/SELECT
round-trip against the booted server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
The earlier approach booted a standalone `clickhouse server`, but that binary
(26.3.5.12, macOS) crashes ~deterministically at startup with a null-pointer in
Context::setClustersConfig during initial config load — reproduced independent of
config content (minimal/full, ±remote_servers, ±jemalloc env). It's a ClickHouse
binary bug, not our config, so spawning a server locally is a dead end.

It's also unnecessary: the app's credentials login already takes a ClickHouse
host and queries it cross-origin (app.js connect() → resolveTarget → chCtx.origin).
So the local runner only needs to serve the static page; you point the login at
whatever ClickHouse you want (local or remote).

- build/local.py: tiny stdlib HTTP server — serves dist/sql.html at /sql and a
  {basic_login:true} config.json at */config.json. No deps, no ClickHouse, no
  proxy. PORT overridable (default 8900).
- Remove build/local.mjs (the clickhouse-server launcher) and the /.local-ch/
  gitignore; `npm run local` = build + python3 build/local.py.
- README: "Run locally against your own ClickHouse" — sign in with your CH host
  (include the scheme) + credentials; note ClickHouse's default CORS makes the
  cross-origin queries work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
@BorisTyshkevich BorisTyshkevich changed the title feat(dev): npm run local — throwaway local ClickHouse, no Docker feat(dev): npm run local — serve the SPA locally, connect to your own ClickHouse Jun 26, 2026
BorisTyshkevich and others added 2 commits June 26, 2026 19:22
`npm run local` now reads ~/.clickhouse-client/config.xml and offers your
connections as a "Saved connection" dropdown on the login screen — basic
(hostname/user/password) and OAuth (clickhouse-client's oauth-url/oauth-client-id/
oauth-audience keys) alike — so you pick a cluster instead of typing it.

SPA (shipped, config-driven):
- oauth-config.js: config.json gains an optional `hosts[]`; loadConfigDoc returns
  { idps, basicLogin, hosts } with normalizeHost → {label,url,auth,user,password,idp}.
- app.js: OAuth can now target a cross-origin cluster — login(idpId, targetOrigin)
  stashes `oauth_origin` (survives the redirect like oauth_idp/oauth_state), chCtx.origin
  reads it in OAuth mode, clearTokens/sign-out clears it. redirect_uri stays the
  localhost SPA. The `login` action forwards the target origin.
- login.js: a host `<select>` (shown only when config lists hosts) — a basic host
  prefills host/user/password + opens Advanced; an OAuth host starts SSO against that
  cluster via login(idp, url). styles.css: .login-picker.

local glue:
- build/local.py: parse connections_credentials → hosts[] (basic with autofilled
  user/password; a connection with oauth-* keys → an oauth host + an IdP entry,
  bearer=access_token when an audience is set). Serves the generated config; URL derived
  from secure/hostname/http_port. LOCAL_CH_CONFIG overrides the path.
- README: "Run locally" updated for the picker + OAuth redirect/CORS prerequisites.

Tests: oauth-config (hosts normalization), app (oauth_origin stash/read/clear), login
(picker render, basic prefill, oauth sign-in, placeholder no-op, error path). 946 tests
pass; per-file coverage gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
@BorisTyshkevich BorisTyshkevich changed the title feat(dev): npm run local — serve the SPA locally, connect to your own ClickHouse feat: run locally + host picker from clickhouse-client (basic + OAuth) Jun 26, 2026
BorisTyshkevich and others added 3 commits June 26, 2026 19:43
… config

A Web-client OAuth provider (e.g. Google) requires a client_secret at the code
exchange, but clickhouse-client's oauth-* keys don't include one. build/local.py
now also reads an `oauth-client-secret` key from the connection and passes it into
the generated IdP as `client_secret` (the SPA already forwards it; empty → public
PKCE). This lets `npm run local` complete OAuth against a Google-backed cluster
(e.g. antalya) using the same public-by-design secret the cluster serves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
…SO button)

When config.json lists both `hosts` and the `idps` they reference, the login screen
also rendered a standalone "Continue with <idp>" button. That button does
serving-host SSO — for `npm run local` the serving host is localhost (no
ClickHouse), so the query POSTed to the static server and got a 501. Suppress the
standalone SSO button for any IdP a saved host references; such IdPs are reached
only via the host picker, which sets the cross-origin query target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
…MA filtered)

loadSchema excluded system/INFORMATION_SCHEMA/information_schema from the schema
tree. Drop `system` so it shows alongside user databases (useful for dashboards/
diagnostics); the redundant INFORMATION_SCHEMA views stay filtered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmDqY6Bxf5b1fyQE8XwA2u
@BorisTyshkevich BorisTyshkevich merged commit 0117d78 into main Jun 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant