Read-only, on-demand browser over Tansu's native S3 storage. Topics, events, consumer groups and simple stats are read directly from the object store Tansu writes to — no Kafka broker, no Kafka client, no background polling. Every read is triggered by a user action.
Built with Rust (Axum) + Nuxt 3.
Tansu persists everything to S3 under a known layout (reverse-engineered from
tansu-storage::dynostore):
clusters/{cluster}/meta.json topic/producer/txn metadata
clusters/{cluster}/topics/{topic}/partitions/{p:010}/watermark.json low/high offsets
clusters/{cluster}/topics/{topic}/partitions/{p:010}/records/{base_offset:020}.batch
clusters/{cluster}/groups/consumers/{group}.json consumer group detail
clusters/{cluster}/groups/consumers/{group}/offsets/{topic}/partitions/{p:010}.json
Kotatsu reads these objects via the object_store crate and decodes the
.batch files (raw Kafka record batches) with tansu-sans-io. Avro values are
resolved against Kora (Confluent-compatible
schema registry). See the GitHub issues for the full design.
kotatsu/
├── backend/ # Rust (Axum) — object_store + tansu-sans-io, no Kafka client
├── frontend/ # Nuxt 3 (SPA), served as static assets by the backend in prod
├── Dockerfile # multi-stage → single image (backend serves frontend)
└── docker-compose.yml
Two processes, with the frontend proxying /api to the backend.
# 1. backend
cd backend
cargo run # listens on 0.0.0.0:8080
# 2. frontend (separate terminal)
cd frontend
npm install
npm run dev # http://localhost:3000, proxies /api → http://localhost:8080Environment variables (backend):
| Var | Default | Purpose |
|---|---|---|
KOTATSU_BIND |
0.0.0.0:8080 |
HTTP bind address |
KOTATSU_STATIC_DIR |
(unset) | Dir of built frontend assets (prod only) |
S3 source variables (KOTATSU_S3_*) are consumed starting with the storage
layer (issue #2).
docker compose up --buildStarts the Kotatsu app (backend + bundled frontend) on http://localhost:8080 and
a MinIO S3 on http://localhost:9000 (console at :9001, minioadmin/minioadmin)
with a tansu bucket created automatically.
It also starts a Tansu broker (localhost:9092, cluster demo) writing to
that bucket, so you can generate real events:
# create a topic + produce a few messages with any Kafka client
docker run --rm --network kotatsu_default apache/kafka:latest \
/opt/kafka/bin/kafka-topics.sh --bootstrap-server tansu:9092 \
--create --topic orders --partitions 1 --replication-factor 1
printf 'key-1:{"id":1}\n' | docker run -i --rm --network kotatsu_default apache/kafka:latest \
/opt/kafka/bin/kafka-console-producer.sh --bootstrap-server tansu:9092 \
--topic orders --property parse.key=true --property key.separator=:The records land under clusters/demo/topics/orders/… in the bucket and are
read back by Kotatsu.
The stack also runs Kora (Confluent-compatible schema registry) on
localhost:8085 with its own PostgreSQL; the app resolves Avro schemas via
KOTATSU_KORA_URL=http://kora:8080. To produce Confluent-framed Avro events
(schema auto-registered in Kora):
printf '{"id":1,"item":"widget"}\n' | docker run -i --rm --network kotatsu_default \
confluentinc/cp-schema-registry:7.6.0 kafka-avro-console-producer \
--bootstrap-server tansu:9092 --topic avro-orders \
--property schema.registry.url=http://kora:8080 \
--property value.schema='{"type":"record","name":"Order","fields":[{"name":"id","type":"int"},{"name":"item","type":"string"}]}'Kotatsu decodes these in the event browser and lists the schema under Schemas.
To build the single production image on its own:
docker build -t kotatsu .
docker run -p 8080:8080 kotatsuPushed images are published to ghcr.io/popsink/kotatsu by the release
workflow on every push to main (tagged main + sha) and on v* tags
(semver + latest), built for linux/amd64 and linux/arm64.
docker run -p 8080:8080 ghcr.io/popsink/kotatsu:latestA Helm chart is published as an OCI artifact to
oci://ghcr.io/popsink/charts/kotatsu by the chart-release
workflow.
helm install kotatsu oci://ghcr.io/popsink/charts/kotatsu --version 0.1.1 \
--set s3.bucket=tansu \
--set s3.cluster=demo \
--set koraUrl=http://kora:8080s3.cluster and s3.bucket are required. Provide static keys via
s3.accessKey/s3.secretKey, or omit them to use the pod's IAM role — attach
it through serviceAccount.annotations (EKS IRSA) or a Pod Identity
association. See chart/kotatsu/values.yaml for all
options.
cd backend
cargo test # unit tests (decode, keys, parsing) — no services neededIntegration tests under backend/tests/ are #[ignore]-gated so CI stays
green without infrastructure. They are self-contained (they seed their own
data and clean up), needing only the relevant services running:
docker compose up -d minio createbucket kora kora-db
cargo test -- --ignored # runs s3 / groups / schema integration tests
# or per suite:
cargo test --test groups_integration -- --ignored
cargo test --test schema_integration -- --ignoredSet the bucket/endpoint and the Tansu cluster name (see docker-compose.yml
for the variable names). A single source per instance for now; multi-source
comes later.
| Var | Purpose |
|---|---|
KOTATSU_S3_BUCKET |
bucket holding Tansu's storage |
KOTATSU_CLUSTER |
Tansu cluster id (clusters/{cluster}/ prefix) |
KOTATSU_S3_ENDPOINT |
custom endpoint (MinIO/R2); omit for AWS |
KOTATSU_S3_REGION |
region (default us-east-1) |
KOTATSU_S3_FORCE_PATH_STYLE |
true for MinIO/most S3s; set false for AWS S3 |
KOTATSU_S3_ACCESS_KEY / _SECRET_KEY |
static keys (optional) |
KOTATSU_S3_SESSION_TOKEN |
session token for static temporary creds (optional) |
If KOTATSU_S3_ACCESS_KEY/KOTATSU_S3_SECRET_KEY are set they are used
directly. Otherwise Kotatsu resolves credentials from the ambient AWS chain,
so it can run with no secrets:
- environment —
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN - web identity (EKS IRSA) —
AWS_WEB_IDENTITY_TOKEN_FILE+AWS_ROLE_ARN - EKS Pod Identity / ECS — container credential endpoints
- EC2/ECS instance role — IMDS
Temporary credentials are refreshed automatically. On EKS, attach a role to the
pod's ServiceAccount (IRSA annotation eks.amazonaws.com/role-arn) or via an
EKS Pod Identity association — the platform injects the env above and Kotatsu
picks it up. For real AWS S3 also set KOTATSU_S3_FORCE_PATH_STYLE=false and
leave KOTATSU_S3_ENDPOINT unset.