A pipeline you babysit.
Every hop has its own config, its own retries and its own pager. Numbers drift between the cache and the warehouse, and nobody owns the gap.
Basalt turns Kafka topics, Postgres changes and webhooks into typed tables and APIs in milliseconds. Define it in SQL, ship it from the CLI, read it from anywhere.
$ npx basalt initCopyEvery hop has its own config, its own retries and its own pager. Numbers drift between the cache and the warehouse, and nobody owns the gap.
Sources, views and endpoints live in one project and ship with one command. The API, the dashboard and the model all read the same row.
Kafka topics, Streamtide shards, Postgres change streams, webhooks and SDK calls land in one ordered log. Retries are deduplicated, so every event counts once.
Write a SELECT and Basalt keeps it current with every event. Joins, windows and aggregates update incrementally, never as a nightly batch.
Every cell knows which events produced it. Click a number in the console and walk the path to the raw rows, column by column.
Read a view as of any moment in the last 30 days. Debug an incident with the exact rows your API served at 02:14:07.
Add or retype a field and Basalt versions the schema in place. Old readers keep working while new code picks up the change.
Hot keys get their own partition and quiet ones merge back. Throughput follows traffic with no capacity planning or rebalancing jobs.
Typed SDKs for TypeScript, Python and Go, or plain SQL files the CLI deploys. Each tab below builds the same source, view and endpoint.
import { basalt } from "@basalt/sdk"; const db = basalt({ project: "acme-prod" }); await db.source("payments", { kafka: "payments.v3" }); await db.view("revenue_by_minute", ` SELECT window(ts, '1m') AS minute, sum(amount) AS revenue FROM payments GROUP BY 1`); const rows = await db.table("revenue_by_minute").last(3);
from basalt import Basalt db = Basalt(project="acme-prod") db.source("payments", kafka="payments.v3") db.view("revenue_by_minute", """ SELECT window(ts, '1m') AS minute, sum(amount) AS revenue FROM payments GROUP BY 1""") rows = db.table("revenue_by_minute").last(3)
package main import "github.com/basalt-dev/basalt-go" func main() { db := basalt.New("acme-prod") db.Source("payments", basalt.Kafka("payments.v3")) db.View("revenue_by_minute", ` SELECT window(ts, '1m') AS minute, sum(amount) AS revenue FROM payments GROUP BY 1`) rows, _ := db.Table("revenue_by_minute").Last(3) }
-- views/revenue.sql · deploy with: basalt view apply CREATE SOURCE payments FROM KAFKA 'payments.v3'; CREATE VIEW revenue_by_minute AS SELECT window(ts, '1m') AS minute, sum(amount) AS revenue FROM payments GROUP BY 1; SELECT * FROM revenue_by_minute ORDER BY minute DESC LIMIT 3;
Join two views as of the same second, up to 30 days back.
Typed rows from generated structs, plus context-aware retries.
Logical replication from Postgres 17, partitioned tables included.
A tour of the read path rewrite that shipped with v4.0.
We replaced a Flink job, two Airflow DAGs and a Redis cache with one SQL file. Our on-call rotation noticed before finance did.
basalt deploy is the shortest path from a Kafka topic to an API I have ever used.
Time travel made our incident review easy. We queried the fraud view as of 02:14:07 and saw exactly what the model saw at that second. No log archaeology, no guessing which cache was stale.
The Go SDK reads like it was written by people who write Go.
We moved 41 materialized views over in a weekend. Column lineage made the review boring, which is the best thing a review can be.
p99 holds at 3 ms from Reykjavík to Sydney. I stopped checking the dashboard.
10M events a month, 3 views and community support. Enough for a side project that needs real streaming.
Start free1B events included, unlimited views and sources, a 99.95% SLA and 30 days of time travel.
Start 14-day trialDedicated cells, VPC peering, SSO, a year of history and an engineer who knows your schema.
Talk to usFree up to 10M events a month. No card, no sales call. Your first pipeline is live in about four minutes.