Skip to content

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.

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

The fastest path uses Docker Compose — it launches ClickHouse and a single wavehouse process.

Terminal window
git clone https://github.com/Wave-RF/WaveHouse.git
cd WaveHouse
docker compose -f deployments/compose/standalone.yaml up -d

The 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) and 9000 (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.

WaveHouse uses a Bring Your Own Schema model — you create tables in ClickHouse, and WaveHouse discovers them automatically via system.columns.

Terminal window
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).

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:

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

The trial public role can read the demo tables, so query clicks with the structured-query endpoint — no token needed:

Terminal window
# 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.

Every ingested event is broadcast to SSE subscribers before it’s flushed to ClickHouse, so dashboards see new data with zero perceived lag.

Terminal window
# 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.

The handful of things that most often trip up a first session — each is expected behavior with a quick fix:

  • 404 unknown table: clicks on the first ingest. Schema discovery refreshes every 60 seconds (schema.refresh_interval in 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/refresh forces it, but that endpoint is admin-only — the trial public role 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.)
  • 403 on a table you created yourself. WaveHouse is fail-closed and the trial policy grants the public role 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) and 8123/9000 (ClickHouse). Stop whatever holds the port or edit the ports: mappings in deployments/compose/standalone.yaml.
  • Errors right at first boot. docker compose -f deployments/compose/standalone.yaml ps should 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.
  • 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.
  • Validate JWTs: set auth.jwt_secret in the settings directory’s config.json (deployments/compose/settings/config.json in the quickstart; it reloads live — the middleware always runs, and without a secret or a jwks_url every request is the policy default_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.enabled to true in the settings directory’s config.json (it hot-reloads, no restart) — records dedupe on their event_id field by default; pick a different field (globally or per table) in the same file — see Settings Directory — Deduplication.

Was this page helpful?

Thanks for the feedback!