@cookiemonsterdev/kafka-studio
v1.1.0
Published
A local web UI for inspecting and interacting with an Apache Kafka cluster, built on @cookiemonsterdev/kafka-core.
Maintainers
Readme
@cookiemonsterdev/kafka-studio
A local web UI for inspecting and interacting with an Apache Kafka cluster — browse and manage
topics, tail and produce messages, inspect consumer groups, and watch a live view of traffic
moving through the cluster. Launched from the command line and served on localhost, in the
spirit of Prisma Studio. Built on top of
@cookiemonsterdev/kafka-core and
@cookiemonsterdev/kafka-config, and lives in the
kafka monorepo.
Status: early — the CLI, HTTP server, and web shell run end to end. The studio can browse,
create and configure topics; produce messages (single sends and rate-limited bursts); browse and
tail live messages; inspect consumer groups (members, per-partition lag, offset reset, deletion)
and share groups; render a live topology board of cluster activity; and browse read-only ACL,
client quota, and transaction state. A command palette (⌘K/Ctrl+K) jumps between pages. Every
session is authenticated (see Security), and --read-only is enforced by the server,
not just hidden in the UI. Launchable standalone (kafka-studio, this package's own bin) or as
kafka studio from @cookiemonsterdev/kafka-cli, which resolves this package
at runtime and prints an install hint if it isn't present. Not published to npm; install and usage
instructions will follow.
Contents
Local development
From the workspace root:
pnpm --filter @cookiemonsterdev/kafka-studio build
node packages/studio/dist/bin.jsbuild produces the server bundle (dist/) and the browser SPA (dist/web/). Run
node packages/studio/dist/bin.js --help for the flag list (port, host, browser, read-only mode).
pnpm --filter @cookiemonsterdev/kafka-studio dev watches and rebuilds the server bundle only. To
serve src/web with Vite's own dev server instead of a static build, set KAFKA_STUDIO_DEV=1
before starting the server from source — or run the one-liner below, which does both:
pnpm studio:devBuilds @cookiemonsterdev/kafka-config, @cookiemonsterdev/kafka-core, and the studio itself, then
starts the server with KAFKA_STUDIO_DEV=1 set, so src/web is served through Vite's dev
middleware (live reload) instead of the static dist/web build. Re-run it after a server-side
source change; src/web changes hot-reload on their own.
Local Kafka with Docker
The studio has no built-in cluster — it needs a real (or locally hosted) broker to point at.
docker-compose.dev.yml in this package brings up a single-node KRaft broker on localhost:9092,
PLAINTEXT only, for exactly this:
cd packages/studio
docker compose -f docker-compose.dev.yml up -dWait for it to report healthy (docker compose -f docker-compose.dev.yml ps), then point the
studio at it with KAFKA_BROKERS — the studio (via @cookiemonsterdev/kafka-core's fromEnv)
reads this the same way the CLI does, so no config file is required for a quick local check:
KAFKA_BROKERS=localhost:9092 node packages/studio/dist/bin.jsTo use a named cli.profiles connection instead (SASL/SSL, a remote cluster, …), point
KAFKA_CONFIG at a kafka.config.* file the same way @cookiemonsterdev/kafka-cli does — see
@cookiemonsterdev/kafka-config.
Tear the broker down (and drop its data) with:
docker compose -f docker-compose.dev.yml down -vThis compose file is for manual, local use only — it is not part of pnpm test or
pnpm test:integration for this package. pnpm test:integration instead brings up its own broker
from @cookiemonsterdev/kafka-core's test/assets/ (see
Tests below), reusing those compose files rather than shipping a second copy of them.
Security
A local server that can speak to a possibly-production Kafka cluster is a real attack surface, so none of this is optional:
- Localhost by default. The server binds
127.0.0.1unless you pass an explicit--host. Doing so prints a loud warning to stderr — it means this machine's network can now reach a server that can read and mutate the connected cluster. - A session token per process. On startup the server generates a random token and opens the
browser to a URL carrying it in the hash (
#token=…), never in a query string or header the server itself would log. The page reads it once, keeps it in memory only (notsessionStorageor any other Web Storage, which stays readable by an XSS payload for as long as the tab is open) for the tab's lifetime, and strips it from the visible URL. Every/api/*request after that carries it asx-kafka-studio-token(or, for the SSE streamsEventSourcecan't attach headers to, atokenquery param instead). A request with a missing or wrong token gets401. A hard reload starts a fresh session — reopen the studio from the terminal's own printed URL. - An Origin/Host allowlist. The standard defense against DNS rebinding against a local dev
server: a request naming a
Host(or, when present,Origin) other than the address the server was actually told to bind is rejected with403, before the token is even checked. --read-onlyis enforced by the server, not by hiding buttons in the UI — a mutating request is rejected with403regardless of what the client sent.
Design system
The UI is dark-only — there is no light palette, no .dark class, and no theme toggle. Every
token lives in the single :root block of src/web/styles/theme.css, and
scripts/check-theme-drift.mjs keeps that block byte-identical to
packages/docs/src/styles/global.css. Change one, change both, then run pnpm theme:check from
the workspace root.
The accent (--primary) is the brand mark's own green, and --chart-1 … --chart-5 are a
categorical set — src/web/lib/topic-accent.ts hashes a topic name onto one of them so a topic
keeps the same colour everywhere it appears.
Two colour literals are duplicated by hand because Vite does not process them through the module
graph: the theme-color meta and the splash screen in src/web/index.html. Both mirror
--background; keep them in sync when that token changes.
Loading and failure states go through the shared components in src/web/components/ui/ —
skeleton, spinner, empty-state, error-state, query-boundary and toast. A failed read
gets a skeleton then an error panel with a working retry; a failed mutation always raises a toast
via the QueryClient's MutationCache, so it survives the dialog that started it closing.
Tests
pnpm --filter @cookiemonsterdev/kafka-studio test
pnpm --filter @cookiemonsterdev/kafka-studio test:integrationUnit tests live beside source as src/**/*.test.ts and never start Docker; test/suites/tarball.test.ts
and test/suites/bundle-budget.test.ts are also unit tests in this sense — both build and pack the
package, neither needs a broker. test/suites/studio-e2e.test.ts is the one broker-backed
integration suite, walking the studio's HTTP API (topic create/describe/configure, produce, a
bounded message read, consumer group listing) against a real cluster brought up by
test/helpers/global-setup.ts — the same compose files
@cookiemonsterdev/kafka-core's own integration suite uses, not a second
copy of them. KAFKA_EXTERNAL=1 (point KAFKA_BROKERS at an already-running broker) and
DO_NOT_STOP=1 work the same way here as they do for core and the CLI.
Contributing
See CONTRIBUTING.md at the workspace root.
