Real-time API gateway for ClickHouse | WaveHouseSkip to content<br>SearchCtrlKCancel
GitHub<br>Select themeDarkLightAuto
Open Source · Apache 2.0WaveHouse<br>The open-source real-time API gateway for ClickHouse®. Schema-aware ingest, async batching, real-time streaming, and tiered query caching — in a single binary.<br>Read the docsTry WaveHouse Cloud<br>$pnpm add @wavehouse/sdk— stars · live
stats.wavehouse.devconnecting…<br>—stargazers<br>—forks<br>—events · 7d
⚡ingested → live— tracked<br>connecting to the live stream…
Star Wave-RF/WaveHouseand watch it land here, liveFull demo<br>Queried in your browser by @wavehouse/sdk — one cached pipe()summary, a server-curated pipe() backfill, and a live SSEstream().
1binaryAPI · worker · NATS · dedup<br>mswarm-cache query path, local bench<br>SSEstreamingreal-time push, gap-filled from history<br>Apache 2.0opensame binary, self-host or managed
Why WaveHouse exists<br>Section titled “Why WaveHouse exists”
ClickHouse is a phenomenal OLAP database, but pointing a frontend straight at it comes with sharp edges: custom APIs, Kafka® queues to avoid “too many parts” errors, replacing-merge logic for deduplication. WaveHouse abstracts all of that into a single, deployable binary so you stop interacting with ClickHouse directly.
Frontend → ClickHouse, directlyOne insert per event → Too many parts , HTTP 500 under load<br>No backpressure, no edge validation — bad rows fail late<br>No real-time push — poll every 2s or bolt on Kafka + WebSockets<br>No row/column security — hand-write tenant filters on every query
Frontend → WaveHouse → ClickHouseAsync WAL + batched flush — never “too many parts”<br>Schema-validated at the edge, 503 + Retry-After under load<br>Native SSE push, gap-filled from history — no extra stack<br>Hasura-style JWT row/column policies, built in
If you’re building user-facing analytics, WaveHouse is like Supabase™ for ClickHouse — or an open-source Tinybird™ that pushes data to the frontend in real time over SSE, not just via pull-based REST.
Schema-aware validation<br>WaveHouse discovers your ClickHouse schemas via system.columns and validates every ingest against the real schema — unknown fields, type mismatches, and null violations are rejected at the edge.
Async buffered ingest<br>Writes land in a durable NATS™ JetStream WAL and return 200 OK instantly. A background worker batch-flushes to ClickHouse — never drop a packet.
Real-time push<br>Every event is broadcast to SSE subscribers before it’s flushed to ClickHouse. Gap-fill from JetStream history for late-connecting clients.
In-process query cache<br>Ristretto cache plus Go singleflight coalesces identical concurrent queries — dashboards survive thundering herds without an extra cache tier to operate.
Hasura-style access control<br>Per-table, per-role column and row-level policies with JWT claim templating. Stored in NATS KV with file-based bootstrap and cluster sync.
TypeScript SDK<br>@wavehouse/sdk — a type-safe query builder, live queries, real-time streaming, and codegen from your schemas, with one runtime dependency of ~1.4 KB gzipped.
Plus — optional deduplication (idempotent ingest by ID), a dead-letter queue for failed batch inserts, and Tinybird-style named pipes with parameter binding and per-role restrictions.
Query it like a database. Subscribe to it like a socket<br>Section titled “Query it like a database. Subscribe to it like a socket”
The TypeScript® SDK wraps the whole surface — typed inserts, a chainable query builder, and live queries that backfill history before streaming:
Ingest<br>Query<br>Live updates
import { createClient } from '@wavehouse/sdk';
const wh = createClient({ baseURL: 'https://.wavehouse.app' });
// Returns immediately — buffered in the WAL, schema-validated
// at the edge, batch-flushed to ClickHouse behind the scenes.
await wh.from('clicks').insert({ page: '/home', button: 'signup' });
.wavehouse.app' });// Returns immediately — buffered in the WAL, schema-validated// at the edge, batch-flushed to ClickHouse behind the scenes.await wh.from('clicks').insert({ page: '/home', button: 'signup' });">
// Chainable, type-safe, never throws — branch on { data, error }.
const { data, error } = await wh.from('clicks')
.select('page', 'button', 'score')
.where('page', '=', '/home')
.orderBy('received_timestamp', 'desc')
.limit(100);
// Historical backfill, then the live stream.
const lq = wh.from('clicks')
.selectAll()
.where('page', '=', '/home')
.orderBy('received_timestamp', 'desc')
.limit(100)
.liveQuery({
initial: (result) => setRows(result.data ?? []),
next: (event) => addRow(event.data),
});
// later: lq.close();
setRows(result.data ?? []), next: (event) => addRow(event.data), });// later: lq.close();">
Up in five minutes<br>Section titled “Up in five minutes”
One compose file — ClickHouse + WaveHouse — and you have an ingest endpoint, a query endpoint, and a live event stream:
Terminal window# Boot ClickHouse + WaveHouse with one compose file
git clone https://github.com/Wave-RF/WaveHouse.git
cd...