WaveHouse: Open-source real-time API gateway for ClickHouse

thunderbong1 pts0 comments

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...

wavehouse clickhouse query real time live

Related Articles