Install. Push. Query.
Install the beava server, install the Python SDK, push your first event and query the feature — in five minutes.
Pick a path
Most readers want the install path below; if you'd rather skip the server entirely and run beava in-process under your Python interpreter, embed mode is a one-line alternative.
One docker run and you have an HTTP server on :8080. The whole flow fits in your terminal history.
bv.App() with no URL spawns a local binary on ephemeral ports. Best for tests and notebooks.
This is the SDK reference. For the why — vision, concepts, recipes — see Docs. For tutorials, Learn.
01 Install beava
pip install beava drops a platform wheel from PyPI. The wheel ships the SDK and the Rust server binary together (~4 MB, polars / ruff / uv pattern). If you'd rather run the server in a container, Docker is the alternative.
pip install beava # SDK + server binary land together; `beava` is on PATH beava --data-dir ./.beava/ # → HTTP listen : 127.0.0.1:8080 # → TCP listen : 127.0.0.1:8081 (enabled=true)
The wheel comes from PyPI. The bundled beava binary lands in your Python user-scripts dir (~/.local/bin/beava on Linux, ~/Library/Python/<ver>/bin/beava on macOS), so embed mode (bv.App() with no URL) finds it without extra setup. Pin a specific version with pip install beava==0.0.0. Add --memory-only to skip the WAL entirely.
# persistent volume keeps your features across restarts docker run -d --name beava \ -p 8080:8080 -p 8081:8081 \ -v beava-data:/data \ beavadev/beava:latest # SDK install — same `pip install beava` works inside docker # the bundled binary on PATH; pass an explicit URL to `bv.App(url=...)`. curl -fsSL https://raw.githubusercontent.com/beava-dev/beava/main/scripts/install.sh | sh # verify curl -X POST http://localhost:8080/ping # → {"pong":true,"registry_version":0}
Port 8080 is the HTTP data plane (push, get, batch_get, register, ping). Port 8081 is the framed-TCP fast-path (sub-millisecond JSON). Admin endpoints (/health, /ready, /metrics, /registry) live on a separate sidecar port that you opt into via the --http-addr / --config flags.
Tip. beava ships static. There's no JVM to size, no broker to configure, no schema registry to register against. If curl -X POST :8080/ping returns {"pong":true,"registry_version":0}, you're done.
02 Declare your first feature
beava is declarative. You write a Python class for the event source and a function for the table (the per-entity feature you want to query). Every operator works the same way: group_by(...).agg(name=bv.<op>(...)).
import beava as bv @bv.event class PageView: user_id: str path: str @bv.table(key="user_id") def UserStats(pv: PageView): return pv.group_by("user_id").agg( visits=bv.count(window="1h"), )
Register it once. The server picks it up live — no restart.
python -c "import beava as bv; from site_features import *; \ bv.App('http://localhost:8080').register(PageView, UserStats)" # → {"status": "ok", "registry_version": 1, "added": ["PageView", "UserStats"]}
03 Push events, read the feature
beava speaks HTTP — every operation is a single request. You can drive it from Python or from curl; the wire is identical.
curl -X POST \ http://localhost:8080/push \ -H "content-type: application/json" \ -d '{ "event": "PageView", "data": { "user_id": "alice", "path": "/home" } }'
{ "ack_lsn": 62, "idempotent_replay": false, "registry_version": 1 }
Now ask beava for the running feature:
curl -X POST \ http://localhost:8080/get \ -H "content-type: application/json" \ -d '{ "table": "UserStats", "key": "alice" }'
{ "visits": 2 }
The /get response is the row itself — a flat dict of feature name → value. Cold-start (no events for that key) returns {}, not an error — a cold key is a key with no data.
App parameters
Every bv.App(...) accepts the same shape. Pass an explicit URL or rely on embed mode (no URL → spawn local binary on ephemeral ports).
Server URL. http://... / https://... for HTTP transport, tcp://... for the custom-framed TCP fast-path. None (the default) for embed mode — the SDK locates the beava binary on $PATH and spawns it.
default None
Per-request transport-level I/O timeout in seconds.
default 30.0
Embed-mode only. Sets BEAVA_TEST_MODE=1 in the spawned binary's env so test-only opcodes (OP_RESET) are accepted. Setting test_mode=True against a network URL emits a UserWarning and is ignored.
default False
Common questions
Embed mode — what's the lifecycle?
bv.App() (no URL) spawns the beava binary on ephemeral ports inside a context manager. The subprocess is torn down on __exit__. Calling any wire method on an embed-mode App outside with raises RuntimeError.
with bv.App() as app: app.register(PageView, UserStats) app.push("PageView", {"user_id": "alice", "path": "/home"}) print(app.get("UserStats", "alice"))
Each spawn allocates a unique tmpdir under $TMPDIR/beava-embed-<pid>-<ms>-<hex>/; cleaned up via atexit.
What does the server hold? Does it persist events?
The server holds per-entity aggregates — counters, velocities, sketches, last-seen — keyed by entity. It does not persist the raw events themselves; it persists the aggregates over them via WAL + periodic snapshot.
If you need event replay, keep your raw events somewhere else (S3, your warehouse). beava is the live-feature server, not the event log.
Cold-start for an unknown key — is that an error?
No. app.get("UserStats", "newuser") returns {} (empty dict) with HTTP 200 when no events have ever been pushed for that key. This matches the Redis-shaped contract: a cold key is just a key with no data.
unknown_table IS an error — that means the table name isn't registered.
How does this compare to Redis?
Redis is a key-value store you can build rolling counters on top of (with Lua + ZSETs + a lot of care). beava is a server that already is rolling counters — and velocities, leaderboards, last-N-seen, sketches — without you assembling the primitives.
Where to go next
You've got beava running and a counter ticking. Two reasonable next stops:
53 purpose-built aggregation primitives — counters, sketches, velocities, decay, recency, geo. Same agg(...) shape, different math.
Events vs tables, embed mode, lifetime aggregation, processing-time semantics, global aggregation. The five ideas you need to be productive.
Read the docs →