feat: run locally + host picker from clickhouse-client (basic + OAuth)#52
Merged
Conversation
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
`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
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
npm run localbuilds the SPA, serves it on localhost, and reads your~/.clickhouse-client/config.xmlconnections into a Saved connection dropdown on the login screen — so you pick a cluster instead of typing it.hostname/user/password) → prefills the credentials form (cross-origin HTTP Basic).oauth-url/oauth-client-id/oauth-audience) → an OAuth sign-in against that cluster.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. Noclickhouse serveris spawned (its macOS build has a flakysetClustersConfigstartup crash; see history below), and nothing is proxied.Changes
SPA (shipped, config-driven, tested):
oauth-config.js: optionalhosts[]in config.json →{ idps, basicLogin, hosts }withnormalizeHost.app.js: OAuth can target a cross-origin cluster —login(idpId, targetOrigin)stashesoauth_origin(survives the redirect likeoauth_idp),chCtx.originreads it in OAuth mode, sign-out clears it;redirect_uristays 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 parsesconnections_credentials→hosts[](basic creds autofilled;oauth-*keys → oauth host + IdP,bearer=access_tokenwhen an audience is set) and serves the generated config.PORT/LOCAL_CH_CONFIGoverridable.Prerequisites for OAuth connections (one-time)
http://localhost:8900/sqlas a redirect URI with the IdP.http://localhost:8900on the cluster (ClickHouse sendsAccess-Control-Allow-OriginforOrigin-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