Getting Started
Run WaveHouse locally in under five minutes. WaveHouse ships as a single binary with ClickHouse® as the only external dependency; this walkthrough covers ingest, query, and real-time streaming.
Prerequisites
Section titled “Prerequisites”- Docker® — for running ClickHouse (and optionally WaveHouse itself).
- curl and jq (optional) — for poking the API.
- Go 1.26+ — only required if you want to build from source; skip it for the Docker path below.
1. Start WaveHouse
Section titled “1. Start WaveHouse”The fastest path uses Docker Compose — it launches ClickHouse and a single wavehouse process.
git clone https://github.com/Wave-RF/WaveHouse.gitcd WaveHousedocker compose -f deployments/compose/standalone.yaml up -dThe stack bind-mounts deployments/compose/settings/ as WaveHouse’s settings directory — the hot-reloadable configuration, ClickHouse address included — so there is nothing to seed; edit those files and the running container picks the change up.
This exposes:
- WaveHouse API on
http://localhost:8080 - ClickHouse on ports
8123(HTTP) and9000(native)
WaveHouse is fail-closed — with no policy adopted, every request is denied. So the standalone stack ships a permissive trial policy in the bind-mounted settings directory (deployments/compose/settings/policies.json + roles.json): a non-admin public role that can read and write the demo tables (clicks, events) with no token, so the quickstart just works. It’s not admin — it can’t run raw SQL or reach the /v1/ops/* surface — and it names specific tables, so it grants nothing in a real deployment (those tables won’t exist there). Edit the files on the host and the running container adopts them (see Access Control — The policy lifecycle). It’s deliberately lenient for trialing — a real deployment should tune it: your own roles, real tables, scoped columns, and usually tokens instead of a public default.
2. Create a ClickHouse table
Section titled “2. Create a ClickHouse table”WaveHouse uses a Bring Your Own Schema model — you create tables in ClickHouse, and WaveHouse discovers them automatically via system.columns.
docker compose -f deployments/compose/standalone.yaml exec clickhouse \ clickhouse-client --query " CREATE TABLE IF NOT EXISTS clicks ( page String, button String, score Float64, received_timestamp DateTime64(3, 'UTC') DEFAULT now64(3, 'UTC') ) ENGINE = MergeTree() ORDER BY (page) "Schemas refresh every 60 seconds by default, or on demand via POST /v1/ops/schema/refresh (admin-only). If the first ingest below returns 404 unknown table: clicks, the refresh simply hasn’t picked the new table up yet — wait and retry (worst case the next refresh is a full 60 seconds out).
3. Ingest an event
Section titled “3. Ingest an event”The JWT middleware always runs, but with no secret configured (the default) every request resolves to the policy default_role — which the trial policy from step 1 maps to the public role (granted insert on the demo tables). So you can POST straight to /v1/ingest?table=clicks with no token:
curl -s -X POST "http://localhost:8080/v1/ingest?table=clicks" \ -H "Content-Type: application/json" \ -d '{"page": "/home", "button": "signup", "score": 42.5}'# → {"ok":true}WaveHouse validates the body against the ClickHouse schema before acknowledging. Unknown fields, type mismatches, and missing required columns are rejected with a 400.
4. Query
Section titled “4. Query”The trial public role can read the demo tables, so query clicks with the structured-query endpoint — no token needed:
# Wait ~5 seconds for the batch flush to ClickHouse, then query:curl -s -X POST "http://localhost:8080/v1/query?table=clicks" \ -H "Content-Type: application/json" \ -d '{"columns": ["page", "button", "score"], "limit": 10}'POST /v1/query?table={table} and GET/POST /v1/pipes/{name} are cached — in-process by default, or in a Redis® shared by every instance with cache.backend: redis — with singleflight coalescing, so duplicate concurrent queries hit ClickHouse once. For raw SQL there’s POST /v1/ops/query (an admin escape hatch that never caches, emitting Cache-Control: no-store), but it’s admin-only — the trial public role can’t reach it. To use it, swap the public default for real auth: configure a JWT secret and present a token whose role is the policy admin_role.
5. Subscribe to real-time updates
Section titled “5. Subscribe to real-time updates”Every ingested event is broadcast to SSE subscribers before it’s flushed to ClickHouse, so dashboards see new data with zero perceived lag.
# Specific table (?table= is required)curl -N "http://localhost:8080/v1/stream?table=clicks"
# With historical replay (RFC 3339 timestamp)curl -N "http://localhost:8080/v1/stream?table=clicks&since=2026-03-24T11:00:00Z"Rows arrive positionally, so raw curl output looks like "row":["/home","signup"] rather than named fields. The stream sends an event: schema frame before the first row and again when the column list changes, and a raw client must pair each row against the most recent frame rather than the first.
That re-announcement is not guaranteed in one case: after a gap-fill across a column change, live rows can arrive without a fresh frame (#543). Drop a row whose length disagrees with the last announced list rather than zipping it, and reconnect to resynchronize — an arity check cannot see a same-length change such as a RENAME COLUMN. See the wire format for the frame sequence and the full rule; the TypeScript SDK does all of this for you.
Troubleshooting first runs
Section titled “Troubleshooting first runs”The handful of things that most often trip up a first session — each is expected behavior with a quick fix:
404 unknown table: clickson the first ingest. Schema discovery refreshes every 60 seconds (schema.refresh_intervalin the settings directory), so a just-created table may not be visible yet. Wait and retry — worst case the next refresh is a full 60 seconds out. (POST /v1/ops/schema/refreshforces it, but that endpoint is admin-only — the trialpublicrole can’t call it.)- The query returns
[]right after an ingest succeeded. Ingest acknowledges as soon as the event is durable in the WAL; the batch worker flushes to ClickHouse every few seconds. If you query within that window the rows simply aren’t in ClickHouse yet — re-query after ~5 seconds. (The SSE stream sees events immediately — it’s broadcast before the flush.) 403on a table you created yourself. WaveHouse is fail-closed and the trial policy grants thepublicrole access to the named demo tables only (clicks,events). A new table needs a policy entry — see Access Control for granting roles per table.- A port is already taken. The stack binds
8080(WaveHouse) and8123/9000(ClickHouse). Stop whatever holds the port or edit theports:mappings indeployments/compose/standalone.yaml. - Errors right at first boot.
docker compose -f deployments/compose/standalone.yaml psshould show both services up — ClickHouse takes a few seconds to initialize on a cold start, so give the stack a moment before the first request.
Next steps
Section titled “Next steps”- Architecture — how ingest, query, cache, and streaming fit together.
- API Reference — every endpoint, request/response shape, and error code.
- TypeScript SDK — client with query builder, live queries, and codegen; one runtime dependency.
- Configuration — full YAML + environment variable reference.
- Deployment — Docker images, releases, health checks.
- Development — building from source, running tests, hot-reload workflow.
Going further
Section titled “Going further”- Validate JWTs: set
auth.jwt_secretin the settings directory’sconfig.json(deployments/compose/settings/config.jsonin the quickstart; it reloads live — the middleware always runs, and without a secret or ajwks_urlevery request is the policydefault_role) and replace the shipped trial policy (deployments/compose/settings/policies.json) with a least-privilege one — see API Reference — Authentication and Access Control. - Enable deduplication: set
dedupe.enabledtotruein the settings directory’sconfig.json(it hot-reloads, no restart) — records dedupe on theirevent_idfield by default; pick a different field (globally or per table) in the same file — see Settings Directory — Deduplication.