Skip to content
Open with AI

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

This exposes:

  • WaveHouse API on http://localhost:8080
  • ClickHouse on ports 8123 (HTTP) and 9000 (native)

WaveHouse is fail-closed — with no policy loaded, every request is denied. So the standalone stack ships a permissive trial policy (deployments/compose/dev-policy.yaml, mounted read-only and wired in via WH_POLICY_FILE_PATH): 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 manage policy/pipes — and it names specific tables, so it grants nothing in a real deployment (those tables won’t exist there). It seeds into NATS KV on first boot; after that KV is authoritative (see Access Control — Bootstrapping). 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/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 (L1 Ristretto) with singleflight coalescing — duplicate concurrent queries hit ClickHouse once. For raw SQL there’s POST /v1/admin/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"

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 (WH_SCHEMA_REFRESH_INTERVAL), 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/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 — zero-dependency client with query builder, live queries, and codegen.
  • Configuration — full YAML + environment variable reference.
  • Deployment — Docker images, releases, health checks.
  • Development — building from source, running tests, hot-reload workflow.
  • Validate JWTs: set WH_AUTH_JWT_SECRET=<secret> (the middleware always runs; without a secret every request is the policy default_role) and replace the shipped trial policy (deployments/compose/dev-policy.yaml) with a least-privilege one — see API Reference — Authentication and Access Control.
  • Enable deduplication: set WH_DEDUPE_ENABLED=true and WH_DEDUPE_ID_FIELD=event_id — see Configuration — Deduplication.