tansu
AGENTS.md1.9k starsChanged 2 days agoApache-2.0View on GitHub
CLAUDE.md imports it. Nisshi is a stateless server written in Rust that implements the Apache Kafka® API, with pluggable storage backends: PostgreSQL, libSQL (SQLite), S3/object store, and memory. Schema-backed topics (Avro, JSON Schema, Protocol Buffers) can be written as Apache Iceberg or Delta Lake tables. The project uses just as a task runner (loads .env automatically). Run a single test with nextest: Note: when running nisshi directly (not via docker compose), set AWS_ENDPOINT="http://localhost:9000" in .env.
- Reads credentials
- Installs packages
We split this AGENTS.md into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 37
Run a single test with nextest:
- Distinctive: line 141
Trace to where Kafka enforces it,: not to where it defines the constant.
- Very specific: line 157
Test logs go to logs/<crate-name>/ (one file per test thread, dirs must exist)
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
This file provides guidance to coding agents (Claude Code, Codex, Cursor and others) when working with code in this repository. CLAUDE.md imports it.
Project Overview
Nisshi is a stateless server written in Rust that implements the Apache Kafka® API, with pluggable storage backends: PostgreSQL, libSQL (SQLite), S3/object store, and memory. Schema-backed topics (Avro, JSON Schema, Protocol Buffers) can be written as Apache Iceberg or Delta Lake tables.
- Rust edition 2024, toolchain pinned to 1.98 (
rust-toolchain.toml) - License: Apache-2.0
unsafe_codeis forbidden workspace-wide
Build & Test Commands
The project uses just as a task runner (loads .env automatically).
just # default: fmt, build, test, clippy
just build # build with all features (dev profile)
just build-all # build every target (bins, examples, tests, benches) with all features - use this to verify a change builds cleanly workspace-wide
just test # nextest + doc tests - use this to rerun the full test suite after a change
just test-workspace # cargo nextest run --workspace --all-targets --all-features, excluding fuzz and nisshi-smoke-test
just test-doc # cargo test --workspace --doc --all-features
just test-fast # test-workspace without the tests that need a docker compose service (Postgres, MinIO, Iceberg catalog) - no `just ci` needed
just doc # rustdoc, warnings denied, private items too; pass --open to browse
just clippy # cargo clippy --workspace --all-features --all-targets -- -D warnings
just fmt # cargo fmt --all --check
just shellcheck # shellcheck every tracked *.sh / *.bash script
just lint-workflows # actionlint, zizmor (as workflow-lint.yml runs them) and pinact over .github/workflows; not in `default`, see the recipe for install and token setup
just check # cargo check --workspace --all-features --all-targets
just smoke <engine> # Kafka CLI smoke suite (nisshi-smoke-test) against postgres, sqlite, memory or s3; starts and removes its own broker and services
just ci # (re)starts the docker compose services (postgres, minio, lakehouse) that integration tests depend on, with empty volumes: in a worktree, the worktree's own project; in the main checkout, the shared default project (see Worktrees)
just services-up # start this worktree's own services and write their ports to .env; services-down removes them, services-prune removes those of removed worktrees
just kafka-ref # clone Apache Kafka at the version of nisshi-sans-io/message/ into kafka-ref/ (gitignored), to read as the reference; see Checking Against Apache KafkaRun a single test with nextest:
cargo nextest run --workspace --all-features -E 'test(test_name_here)'Local Development Environment
cp example.env .env # then edit .env as needed (AWS_ENDPOINT for local minio, etc.)
just ci # starts minio, postgres, lakehouse via docker compose
just broker # build + start broker with full infrastructure
just broker-postgres # broker with postgres backend only
just broker-sqlite # broker with sqlite backend only
just broker-memory # broker with in-memory backend only
just broker-s3 # broker with S3/minio backend onlyNote: when running nisshi directly (not via docker compose), set AWS_ENDPOINT="http://localhost:9000" in .env.
Worktrees
- Build output stays per worktree. Each worktree builds into its own
target/. Don't point worktrees at a shared one (CARGO_TARGET_DIR,build.target-dir). Cargo gives a workspace crate the same artifact names in every checkout and decides whether to rebuild by comparing file modification times, so a checkout whose sources are older than another checkout's last build reuses that build, andcargo testruns the other worktree's code. - Seed a new worktree with
just seed-targetbefore its first build. The recipe clones the main checkout'starget/debugcopy-on-write, so the clone takes no extra disk until cargo rewrites a file. It then runscargo clean --workspaceon the clone, so only dependency artifacts carry over and the workspace crates rebuild from this worktree's sources. The recipe seeds no release, profiling or cross-target build, because that clean covers only the dev profile. It does nothing whentarget/debugalready exists, and an interrupted seed leaves notarget/debug, so a rerun starts again. Don't run it while the main checkout builds. Don't bringtarget/into a worktree any other way (.worktreeinclude,worktree.symlinkDirectories, a plain copy): those carry workspace-crate artifacts over, and cargo then reuses another checkout's code as the previous bullet describes. - Each worktree runs its own services. In a linked worktree (not in the main checkout, not when
CIis set, and not whenNISSHI_WORKTREE_SERVICES=0),etc/worktree-services.sh upstartsdb,minioandlakehouse-catalogin the compose projectnisshi-wt-<worktree directory>-<hash of its path>, on loopback ports that Docker picks. It needs Docker,jqandcurl, and names the one that's missing. It then writes.env, starting fromexample.envif there is none:COMPOSE_PROJECT_NAME, theNISSHI_*_PORTSvariables,AWS_ENDPOINT,ICEBERG_CATALOGandNISSHI_TEST_DATABASE_URL. A laterdocker composeorjustcommand in the worktree then uses that project. nextest runs the script as a setup script before thenisshi-broker,nisshi-storage-sqlandnisshi-schematests, so the first test run starts the services. In a worktree,just ciremoves and starts the worktree's own project, andjust brokerandjust serverstart it before the broker; only grafana and prometheus keep fixed ports, so one worktree at a time runsjust broker.just services-up,just services-downandjust services-prune(removes the project of each worktree that is gone) run the script directly. Stacks stay up untilservices-downor a prune. In the main checkout,just ci,just brokerandjust serverstill tear down and restart the default project, which other sessions may be using, so don't run them there to restart services. See CONTRIBUTING.md for colima on macOS. .envis copied into Claude Code worktrees..worktreeincludelists.env, soclaude --worktreeand subagent worktrees get a copy, and the script above rewrites its service settings.
Architecture
ARCHITECTURE.md is the overview for human contributors; this section keeps the details an agent needs. Update both when the architecture changes.
Cargo workspace (members, including the fuzz harness, are listed in the root Cargo.toml), producing a single binary (nisshi) with subcommands: broker (default), cat, topic, user, generator, perf, proxy.
Key Crates
| Crate | Role |
nisshi | Binary entry point, subcommand dispatch |
nisshi-broker | Kafka API broker: Broker<G, S> generic over Coordinator + Storage |
nisshi-sans-io | Code-generated Kafka wire protocol (pure serde, no I/O) |
nisshi-service | Network service layers built on rama (Layer/Service composition) |
nisshi-storage | Storage abstraction: Storage/StorageFactory traits, StorageContainer builder |
nisshi-storage-dynostore | Storage factories for memory://, s3://, gs:// (ObjectStore-backed) |
nisshi-storage-null | Storage factory for null://, a stub backend with no message persistence |
nisshi-storage-slatedb | Storage factory for slatedb:// (SlateDB, a KV store on object storage) |
nisshi-storage-sql | Storage factories for postgres:// and sqlite:// (PostgreSQL, libSQL) |
nisshi-schema | Schema registry + Iceberg/Delta/Parquet lake integration |
nisshi-client | Async Kafka protocol client (rama service layers) |
nisshi-model | Kafka JSON protocol definitions (used in build.rs) |
nisshi-auth | SASL authentication: mechanism handshake, SCRAM credential verification |
nisshi-cat | CLI: produce/consume Avro, JSON, Protobuf messages |
nisshi-cli | Clap-based CLI argument parsing |
nisshi-generator | CLI: generate fake data for schema-backed topics |
nisshi-otel | OpenTelemetry common configuration |
nisshi-perf | CLI: performance producer/consumer |
nisshi-proxy | Apache Kafka®-compatible proxy |
nisshi-topic | CLI: create, list, delete topics |
Sans-I/O Code Generation (nisshi-sans-io)
nisshi-sans-io/build.rs reads ~185 official Kafka JSON message descriptors from nisshi-sans-io/message/*.json and generates typed Rust structs for every request/response pair. Do not manually edit generated files. The message JSON files are from upstream Apache Kafka.
The generator also emits #[derive(arbitrary::Arbitrary)] on every generated type, gated behind the arbitrary feature flag (dep:arbitrary, see fn arbitrary_field_attribute in build.rs) — this is what lets the fuzz crate construct arbitrary Request/Response values directly from fuzzer bytes instead of fuzzing only the wire decoder. A handful of fields need a hand-written #[arbitrary(with = ...)]/#[arbitrary(default)] attribute to stay sound (e.g. raw-byte fields route through nisshi-sans-io/src/arbitrary_support.rs rather than deriving); see the doc comment on arbitrary_field_attribute for the full list and why each one is needed.
Service Layer Pattern (nisshi-service)
Uses rama crate for Layer/Service composition:
TcpBytesLayer(TCP) ->BytesFrameLayer(bytes -> Kafka Frame) ->FrameRouteService(route to typed handlers) ->FrameBytesLayer->BytesTcpService- Same layering pattern used for broker, proxy, and CLI clients
rama 0.4.0: Context<State> replaced by Extensions
As of rama 0.4.0, Service::serve takes a single req parameter — the old Service<State, Req>::serve(&self, ctx: Context<State>, req: Req) two-parameter form and generic State are gone. Any ambient data (auth state, cluster id, maximum frame size, etc.) that used to travel via Context<State> now travels as a rama::extensions::Extensions bag carried inside the request wrapper type — see nisshi-sans-io/src/input.rs for FrameInput, BodyInput, RequestInput<Q>, and BytesInput, each a { value, extensions: Extensions } pair.
Two things about Extensions are easy to get wrong when touching this code:
- It is not fresh per request.
ExtensionsisArc-backed;.clone()shares the same underlying store, and it is typically created once per connection (or per long-lived session, e.g. a consumer group) and reused/cloned across every request on it — not reconstructed per request the way the old per-callContextwas. Code that inserts something once (e.g.AuthenticationExtensioninBytesFrameService) must checkextensions.contains::<T>()before inserting, since on the 2nd+ request on the same connection it will already be there. Don't assert it's absent — that's a real invariant violation waiting to break multi-round-trip flows like SCRAM. .fork()vs.clone():.fork()creates an isolated child scope — reads fall through to the parent, but inserts land only on the child and don't leak back up. Use.fork()when issuing an internal/side request that should see the caller's extensions but not mutate them (e.g.nisshi-proxy's internalDescribeConfigsRequestlookup). Use.clone()(sharing the same store) when the request is part of the same logical session and should accumulate/observe state alongside sibling requests (e.g.ConsumerGroupService's per-sessionExtensionsfield).Extensionrequires an explicit impl (or#[derive(Extension)]) — there is no blanket impl for arbitraryT, so a generic type can't automatically be stored as an extension unless its concrete implementors opt in.
Per-handler dependencies (the Coordinator in nisshi-broker/src/broker/group/*.rs, the Storage handle G in nisshi-storage/src/service/*.rs) are still injected as plain struct fields (struct FooService<C> { coordinator: C }) on each handler rather than through the Extensions bag — that's deliberate, not a leftover of the migration.
Storage Backends (nisshi-storage, nisshi-storage-dynostore, nisshi-storage-null, nisshi-storage-slatedb, nisshi-storage-sql)
Selected at compile time via feature flags, dispatched at runtime through a StorageFactory trait, not an enum: StorageContainer::builder() (nisshi-storage/src/lib.rs) collects one Arc<dyn StorageFactory> per enabled backend, and .build() picks the first whose scheme() regex matches the configured storage URL. Each backend crate implements the trait in its own factory.rs:
memory://,s3://,gs://- ObjectStore-backed (nisshi-storage-dynostore, feature:dynostore)null://- stub backend, tracks topic metadata only, no message storage (nisshi-storage-null, always compiled in)postgres://- PostgreSQL (nisshi-storage-sql, feature:postgres)sqlite://- libSQL/SQLite (nisshi-storage-sql, feature:libsql)slatedb://- SlateDB KV store (nisshi-storage-slatedb, feature:slatedb). Deprecated. Only security fixes are accepted. When writing code, reviewing a PR or running a sweep, do not suggest or make bug fixes, refactors or feature changes for it, and do not flag non-security issues in it. Leave its tests and backend coverage as they are. Changes needed to keep it compiling and its tests passing after a workspace-wide change (aStorageorStorageFactorytrait change, a shared dependency bump) are still expected, and so are routine Dependabot bumps of its dependencies.
RequestChannelService's Storage trait impl (nisshi-storage/src/service.rs) sends a Request over a channel and extracts the matching Response variant; every method does this via the serve_and_extract!(self, request_expr, ResponseVariant) macro rather than repeating the match/unwrap boilerplate - add new methods the same way.
Broker Specifics
- Node ID is always 111 (single-node, stateless design - this is intentional)
- Group coordination in
nisshi-broker/src/coordinator/group/ EnvVarExp<T>wrapper allows CLI args with${VAR}references expanded at parse time- All
Errortypes implementClone(non-Clone errors wrapped inArc)
Checking Against Apache Kafka
Kafka's Java broker is the reference implementation, and no other spec exists. Compare against it when you plan, implement or review anything that a Kafka client can observe: the wire format, API version ranges, error codes, limits and defaults, what a client may do before it authenticates, and when the broker closes a connection.
- Pin the version. The message descriptors in
nisshi-sans-io/message/are Kafka 3.9.1, andApiVersionsadvertises their full ranges, so read Kafka at the3.9.1tag.just kafka-refclones it intokafka-ref/: a blobless clone with full history, kept in the main checkout, and a linked worktree gets a symlink to it. Useorigin/trunkin that clone only to ask whether something has changed since 3.9.1, and name the version you read. - Trace to where Kafka enforces it, not to where it defines the constant. Find what the client sees on failure: an error code, a response, or a closed connection, and any delay.
- Classify each difference as one of: matches; stricter (check that no real client breaks); looser (needs a reason); or not applicable by design (node ID 111, a stateless broker, storage). Check Kafka's fix history in the same area (
git -C kafka-ref log -- <path>) for bugs that Kafka already hit. - Evidence order: running both beats reading code, and code beats docs. Run Kafka from the
apache/kafka:3.9.1image, and the compat and smoke tests run real clients against Nisshi. A KIP shows intent, not what shipped. - Record it. Cite permalinks pinned to the tag (
https://github.com/apache/kafka/blob/3.9.1/...), never trunk, as.claude/rules/comments.mddescribes.SaslHandshakeService::serveinnisshi-auth/src/handshake.rsshows the link style. For a deliberate difference, comment at the code site with what Kafka does and why Nisshi differs.
Feature Flags
Default: dynostore, postgres, libsql, slatedb. The just build/release feature lists below omit turso, which the broker and storage crates enable by default. Full build: delta,dynostore,iceberg,libsql,parquet,postgres,slatedb. The nisshi crate's own defaults, which cargo install nisshi builds, are dynostore, libsql and postgres; its tests check that they include the broker's default memory:// engine.
Lake features: parquet, iceberg, delta - enable writing schema-backed topics to data lake tables.
Testing Notes
- Tests use
cargo-nextest(notcargo testfor workspace tests) - CI runs nextest with the
ciprofile in.config/nextest.toml(NEXTEST_PROFILE=ci), which writes a JUnit report totarget/nextest/ci/junit.xml; local runs use the default profile. Both kill a test still running after 5 minutes, so a hung test doesn't hold its connections. The default profile runs at most 4 Iceberg tests (nisshi-schema'sberg) at once - Tests that use PostgreSQL read its URL from
NISSHI_TEST_DATABASE_URL(defaultpostgres://postgres:postgres@localhost). A test that creates an Iceberg namespace drops it, with its tables, before it checks its results, so the catalog doesn't grow with each run - Test logs go to
logs/<crate-name>/(one file per test thread, dirs must exist) - Integration tests require external services (postgres, minio, lakehouse): in a linked worktree, the first test run starts the worktree's own (see Worktrees); in the main checkout,
just cistarts them. Rerunjust ciif those services are in a bad state, thenjust testto rerun the suite - Without Docker or the services (an agent sandbox, a laptop without Docker, or a quick loop that shouldn't start a stack), run
just test-fastandjust test-doc, which need no service.just test-fastrunsjust test-workspacewith thefastprofile in.config/nextest.toml, whose filter leaves out thenisshi-brokertests that need Postgres and thenisshi-schematests that need the Iceberg catalog and MinIO, and it setsNISSHI_WORKTREE_SERVICES=0, so a worktree's run starts no services. Runjust testbefore you ask for review; CI runs every test. - The
fastfilter finds the tests that need a service by name, so place a new one where the filter sees it: anisshi-brokertest that needs Postgres goes in apgmodule (by hand or withbackend!(pg, "postgres", ...)), and anisshi-schematest that needs the Iceberg catalog goes in thebergtest target. A test that skips itself when its service is down (as thenisshi-storage-sqlPostgres tests do) can stay in the fast tier. - Tests load
.envviadotenv().ok() - Tests in
nisshi-brokerrun against multiple backends: InMemory, Lite (libSQL), Postgres, SlateDb nisshi-broker,nisshi-sans-ioandnisshi-serviceeach build one integration-test binary,it. To add a test file, createtests/it/<name>.rsand declare it withpub mod <name>;intests/it/main.rs; Cargo ignores undeclared files, and theevery_test_file_is_declaredtest fails if one is missed. Gate backend-specific tests with#[cfg(feature = "...")]on a module, notrequired-features. Run one file's tests with a name filter, e.g.cargo nextest run -p nisshi-broker --all-features -E 'test(/^fetch::/)'- Give each feature its own test file. Put the tests of one feature or one Kafka API in one file under
tests/it/, named after it. Tests for a new feature go in a new file, not at the end of a file about another subject. A reader then finds them by name, and a name filter on the module selects them. - Assert a pattern with
assert_matches!(value, pattern)from std (use std::assert_matches;, stable since Rust 1.96, no crate needed), notassert!(matches!(value, pattern))or theassert_matchescrate. On failure it prints the value, and it reads as one assertion. Nothing enforces this yet, and some older tests still useassert!(matches!(..)); leave those alone unless you're already editing them. nisshi-smoke-testruns the real Kafka CLI tools (in a$KAFKA_IMAGEcontainer) against a broker, and stays out ofjust test; run it withjust smoke <engine>. Its containers use the host network, so on macOS it needs Docker Desktop with host networking turned on, or, under colima,NISSHI_SMOKE_DOCKER_HOST_IP=192.168.5.2, which gives the Kafka CLI container its own network and sends its connections to127.0.0.1to this machine. Its tests talk to the shared broker throughKafkaCli::shared(), or start their own withBroker::isolated(), and each uses its own topic and group names (nisshi_smoke_test::unique::name). A test that applies only to some engines is gated with#[cfg(feature = "<engine>")]or#[cfg(any(...))]on the engines' features, never withcfg(not(...)), becausejust clippyturns every feature on and would never compile it. The test files are grouped in folders undertests/smoke/:main.rsdeclares each folder's module, and<folder>.rsdeclares each file in it, whichevery_test_file_is_declaredchecks. S3 runs onlytopic_admin::topic_lifecycle, selected by thesmoke-s3profile in.config/nextest.toml. A test that fails because of an open bug is marked#[ignore = "<what is broken>"], or#[cfg_attr(feature = "<engine>", ignore = "...")]if it fails only on some engines. CI skips it, local runs still run it (run.shpasses--run-ignored alloutside CI), and the change that fixes the bug removes the attribute- Single-file test targets with specific feature requirements (e.g.
nisshi-schema'sberg) userequired-featuresin theirCargo.toml
Fuzz Testing (fuzz)
The fuzz crate (cargo-fuzz + libfuzzer-sys) is a dev-only workspace member, excluded from just test-workspace. CI runs its targets through fuzz.yml (see CI Pipeline below), and you can run any of them locally. It requires the nightly toolchain, which just targets invoke explicitly (cargo +nightly fuzz ...); you don't need to switch your default toolchain.
just cargo-fuzz +args # cargo +nightly fuzz {{args}}, with LTO off as in fuzz-ci
just fuzz-request-decode # fuzz_request_decode, 60s
just fuzz-member-metadata # fuzz_member_metadata, 60s
just fuzz-generate-seed # (re)generate the corpus seed files, see below
just fuzz-ci <tier> [target] # the targets of a tier in fuzz/ci-targets.json, each for its budget
cargo +nightly fuzz list # list all targets
just cargo-fuzz run <target> -- -max_total_time=60 # fuzz one target
just cargo-fuzz run <target> fuzz/artifacts/<target>/<crash-file> # replay a saved crashThree target shapes, all in fuzz/fuzz_targets/:
fuzz_request_decode/fuzz_response_decode/fuzz_varint/fuzz_deflated_batch/fuzz_member_metadata— fuzz raw bytes straight through the wire decoder/encoder, noStorageinvolved.fuzz_<api>_storage(the majority, ~22 targets, one per Kafka API likefuzz_fetch_storage,fuzz_create_topics_storage) — build an arbitrary typed request (vianisshi-sans-io'sarbitraryfeature) and run it end-to-end through the real*Serviceagainst an in-memoryStorageContainer(storage starts empty every execution), to catch panics/invariant violations in service and storage logic rather than the decoder.generate_seeds(plainfn main, not a fuzz target) — writes hand-built binary seed files intofuzz/corpus/<target>/; rerun it after adding a new seed case.
fuzz/src/lib.rs provides the shared check_request/check_response helpers used by most storage targets: they encode a value at its API's latest version, decode it, re-encode the decoded value, and decode again — then assert the two decodes are equal. They deliberately do not assert the first decode equals the original fuzzed value: a sans-io codec is not required to be lossless against an arbitrary in-memory value (e.g. every generated sequence field is Option<Vec<T>> regardless of wire nullability, so a mandatory array left at None is wire-equivalent to Some(vec![]) but not PartialEq). The round-trip property that must hold is that re-encoding an already-decoded (version-normalized) value decodes back to exactly the same thing. normalize_request returns that decoded value as a typed request, so a target can serve the service what a client could actually send instead of the raw fuzzed value (fuzz_list_offsets_storage does).
fuzz/corpus/, fuzz/artifacts/ (crash reproducers) and fuzz/.gitignore's target/coverage are all untracked — rebuild the corpus locally with just fuzz-generate-seed plus whatever cargo +nightly fuzz run accumulates. If you fix a bug found by fuzzing, replay every existing artifact for that target (just cargo-fuzz run <target> fuzz/artifacts/<target>/<crash-file>) to confirm the fix, not just the one input that was being worked on.
CI Pipeline
GitHub Actions (.github/workflows/ci.yml) runs in two tiers, gated by ci-gate, the single required check that fans in every other job:
- Tier A, every pull_request push:
fmt,clippy(which also runsjust doc),typos,shellcheck,image-ca-certificates(builds the Dockerfile'sbasestage and fails if the image would trust a CA in.github/scripts/distrusted-roots.txt),third-party-license,test(postgres:16, the oldest version the project tests), the memory leg ofcompat-librdkafkaandcompat-franz-go. - Tier B, once per merge-queue entry (
merge_group); a push tomainruns only the cache-saving subset: thebuild-storagematrix (one build per storage engine,tursoincluded) andbuild-storage-lake(one build per lake format ondynostore),teston postgres:16 only, the postgres and sqlite compat legs (S3 compat runs in the nightly Tier C, which also runsteston Postgres 17, 18 and 19beta4), thecompat-librdkafkalegs at the oldest librdkafka version CI tests (v2.3.0) on every engine, experimental until they have a record in CI,cargo-publish-dry-run,src,release(the static Linux binaries),macos(aarch64-apple-darwin: a compile-onlycargo check, and the full release build only on av*tag; the scheduled Tier C workflow (twice a day) builds the macOS release on main),smoke(thenisshi-smoke-testsuite: Kafka CLI tools from Kafka 3.9 and 4.3 against an image built in the job fromrelease's binaries on postgres, sqlite, memory and s3, on x86 and arm),smoke-oldest-client(the same suite with the Kafka 3.7 CLI tools, the oldest client CI tests, on postgres, sqlite, memory and s3 on x86 only);smoke-reportputs every leg of both in one pass/fail grid in the step summary. The push tomainrepeats a tree that the queue already tested, so it runs onlyfmt,clippy,test,cargo-publish-dry-run,release,macosandthird-party-license(the jobs that save a cache), pluschanges,typos,shellcheck,image-ca-certificatesandci-gate. Every other job carriesgithub.ref != 'refs/heads/main', andci-gatenames the jobs that may skip there, so a new job must run on main until someone adds it to that list. - Publishing, from the scheduled Tier C run on main and on
v*tags only:ci.ymlpushes no image. The scheduled Tier C workflow (tier-c.yml) pushes the multi-archedgeimage of main's tip, also taggedmainfor external consumers until a follow-up drops that tag. Itsedge-gatejob requires a successful merge-queue run and a successful push run ofci.ymlfor the tip, andedge-publishbuilds the image from theimage-*binaries of that push run, so nothing compiles. If a merge lands during the nightly run, or its push run is still in progress, the gate skips the publish andedgetrails the tip until the next run. Av*tag doesn't triggerci.yml: it runsrelease.yml, which callsci.ymlthroughworkflow_call(so the full Tier B run happens on the tagged commit), anon-maincheck that the tagged commit is on main, arelease-tagcheck (.github/scripts/check-release-tag.sh, tested inworkflow-lint.yml) that the tag's version is the tagged commit'sCargo.tomlversion and that a stable tag has aCHANGELOG.mdsection, andrelease-readiness(release-readiness-check.yaml), which starts onceon-mainandrelease-tagpass, re-runsguardian-scan.yamlin trunk mode on the tag and blocks on the Guardian gate's verdict. Only when all four pass does it publish, one job after another:publish-image(semver andlatest),github-release(holdscontents: write, runs in thereleaseenvironment, uploads the tarballs, and sets no body), thenpublish-crates. The/cut-releaseskill (.claude/skills/cut-release/) opens the release PR, tags its squash commit, and after the run sets the release body withgh release edit. If onlyrelease-readinessfails, anoverridejob waits in therelease-overrideenvironment, whose required reviewers are repo admins: approving it publishes over the failed check, a Guardian block and a scanner outage alike, and rejecting it ends the run with nothing published. The environment prevents self-review and admin bypass, and theoverridejob refuses to run if either is off or if the run records no approval ofrelease-overridefrom someone other than whoever pushed the tag or started the re-run, so a second admin has to approve: whoever pushed the tag or started a re-run can't. The on-main check does not stop a tag whose commit edits the workflow, so a tag ruleset is the control for who may createv*tags. No other run pushes anything, and a failed leg can't leave a partial release.releaseneeds onlytypos(andchanges), andsmokeneeds onlyrelease, sotest,macosand the build matrices run beside therelease->smokechain instead of ahead of it.testdoesn't wait forfmtorclippy. - Cache:
testsaves the Rust cache under thedevshared key on main; the compat jobs restore it and never save.build-storageandbuild-storage-lakeuse no cache, becausecargo checkreuses little of a full build. rust-cache hashes everyCARGO*,CC*,CFLAGS*,CXX*,CMAKE*andRUST*variable into the key, so setRUST_BACKTRACEandRUST_LOGon the step that needs them, not on the job. - Reports: the
testjobs (inci.ymlandtier-c.yml) run nextest and the doctests as separate steps, because doctests write no JUnit report; the doctests also run after a nextest failure. Both jobs then run the.github/actions/test-reportscomposite action, so a PR'sci.ymlrun exercises the steps the nightly run uses. Itsjunit-summary.pyturns the JUnit report into the job's step summary: results per suite, the 10 slowest tests, and up to 30 failures, each with its message or the end of its stderr. The action uploads atest-reports-<postgres image>artifact withjunit.xmlandcargo-timing.html.ci-gateadds a table of job and step durations to its summary and uploads it as thetimingsartifact; it needsactions: read, sorelease.yml'scijob grants it too. Every report step continues on error, so a report never changes a job's result.
Merging goes through a merge queue: "Merge when ready" queues the PR, the queue re-runs CI on it against the current tip of main, and squash-merges it if everything is green: the PR title becomes the commit's subject and the PR description its body. Tier B is skipped on same-repo PRs only while the MERGE_QUEUE repository variable is on; with it unset, they run everything. Fork PRs can't read the variable, so Tier B is always skipped on them and runs in the queue. The other required checks come from codeql.yml, workflow-lint.yml, dependencies.yml and pr-title.yml (the PR title must follow Conventional Commits; see CONTRIBUTING.md).
On a pull_request, a changes job in ci.yml, codeql.yml, dependencies.yml and fuzz.yml skips the Rust jobs when every changed path is CI config those workflows don't read, or prose. The skip list is .github/scripts/ci-changes.sh, tested by ci-changes.test.sh; the changes jobs run the base branch's copy, so a PR can't widen its own skip list.
guardian-scan.yaml reports supply-chain findings to Guardian, and its Guardian scan gate job is the one check a ruleset requires from it. On a PR it runs FOSSA's licensing and vulnerability checks in report-only mode and uploads nothing. Fork PRs, Dependabot PRs and merge-queue entries skip the scans and pass the gate with a logged reason. On push to main it also builds the musl image (with cargo auditable, like release), scans it with Prisma, syncs both results to Guardian and blocks on Guardian's vulnerability gate, and then Update release manifest writes the manifest row if the commit is still main's tip. .github/scripts/guardian-scan-context.sh decides the mode (pr, queue, trunk, branch) and guardian-scan-gate.sh decides the gate's verdict. Each has a self-test, and both run at the start of every scan. The scripts run from the workflow's own commit, never from the ref being scanned, and a dispatched tag must be spelled refs/tags/<tag> to count as a release (a bare v1.2.3 is treated as a branch). Its secrets are split across two environments. guardian holds only FOSSA_API_KEY, for PR runs. guardian-trunk holds that plus GUARDIAN_URL, GUARDIAN_API_TOKEN and the three PRISMACLOUD_* credentials, and its deployment policy admits only the main branch and v* tags. Setting the FOSSA_LICENSING_BLOCK repository variable to false makes the licensing check report-only on trunk while the Guardian gate stays on. The repository also needs read access to the maas-build-actions package, the solace-cloud-manifest-rw custom property set to true, and the OIDC sub customization [repo_property_solace-cloud-manifest-rw, repository_owner, ref], which the manifest role's trust policy matches for the main branch only.
fuzz.yml runs the cargo-fuzz targets in fuzz/: briefly on every PR (report-only, not a required check) and for longer nightly. Which targets run in each, and for how long, is set in fuzz/ci-targets.json; see fuzz/README.md. Run the PR set locally with just fuzz-ci pr.
Key Files
| File | Purpose |
justfile | All build/test/run tasks |
example.env | Template for local .env config |
compose.yaml | Docker Compose: postgres, minio, grafana, jaeger, prometheus, lakehouse |
nisshi-storage-sql/src/pg/schema.sql | PostgreSQL schema DDL that the broker applies at startup |
etc/schema/ | Sample schemas: .avsc (Avro), .json (JSON Schema), .proto (Protobuf) |
nisshi-sans-io/message/ | Kafka JSON protocol descriptors (upstream, ~185 files) |
nisshi-sans-io/build.rs | Code generator: JSON descriptors -> Rust types |
fuzz/ | cargo-fuzz targets exercising the sans-io codec and storage services (see Fuzz Testing) |
Lint Configuration
Workspace-level in Cargo.toml: clippy::all = warn, unsafe_code = forbid, non_ascii_idents = forbid, rust_2018_idioms = deny, unreachable_pub = warn, broken_intra_doc_links = deny, private_intra_doc_links = deny. CI runs clippy -- -D warnings (all warnings are errors) and just doc, which runs rustdoc with warnings denied.
Trending
Quiet since July.about the whole repository
Who's talking about tansu-io/tansu
- @rmoff.net“it's not like-for-like but github.com/tansu-io/tansu is similar”Bluesky · Oct 30, 2024 · 8 likes
- @rmoff.net“Still wrapping my head around this one. Sounds fun though! tansu-io/tansu: Tansu is an Apache Kafka API …”Bluesky · Sep 17, 2024 · 7 likes
- @stanislavkozlovski.bsky.social“The problem with building that is that... it exists! github.com/tansu-io/tansu”Bluesky · Nov 8, 2025 · 6 likes
- @rusttrending.bsky.social“tansu-io / tansu: Apache Kafka® compatible broker with S3, PostgreSQL, SQLite, Apache Iceberg and Delta …”Bluesky · Jan 4, 2026 · 5 likes
- A Hacker News story“Tansu: A Kafka Compatible Broker with a PostgreSQL Storage Engine”Hacker News · Sep 16, 2024 · 4 points
The biggest 5 of 30 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 30
Talked about on GitHub, Bluesky and Hacker News (30 times).
On GitHub · 17
- Jul 11, 2026Plain SASL authentication may be reacheable due to Cargo feature unification and plain passwords are not checked — nisshi-io/solace-nisshi1 reaction · 1 comment
- Jul 11, 2026SCRAM enforcement — nisshi-io/solace-nisshi1 reaction · 1 comment
- Mar 7, 2026Support TLS/mTLS for client connections — nisshi-io/solace-nisshi1 reaction
- Jan 8, 2026Command error handling — nisshi-io/solace-nisshi1 reaction
- Dec 12, 2025href error in docs — nisshi-io/solace-nisshi1 reaction · 1 comment
12 more on GitHub
- Jun 25, 2026Minimal change to use binary search for dynostore fetch/list_offsets — nisshi-io/solace-nisshi0 reactions · 3 comments
- Jun 18, 2026Integrate upstream changes — Popsink/kodansu0 reactions
- Apr 21, 2026tansu: init at 0.6.0 — NixOS/nixpkgs0 reactions · 1 comment
- Mar 27, 2026return dyn Buf when encoding rather than Bytes — nisshi-io/solace-nisshi0 reactions · 1 comment
- Mar 7, 2026Support PostgreSQL mTLS client authentication for backend connections — nisshi-io/solace-nisshi0 reactions
- Jan 17, 2026chore: upgrade arrow from 55 to 57 — nisshi-io/solace-nisshi0 reactions · 2 comments
- Dec 29, 2025use ExpiringSizedCache in metadata::Cache — nisshi-io/solace-nisshi0 reactions
- Dec 8, 2025500 slatedb storage engine — nisshi-io/solace-nisshi0 reactions · 12 comments
- Dec 5, 2025deadpool pg statement caching — nisshi-io/solace-nisshi0 reactions · 2 comments
- Nov 21, 2025Support rewriting Sasl capability when server does not support it — grepplabs/kafka-proxy0 reactions · 1 comment
- Nov 16, 2025compact policy for lite and pg — nisshi-io/solace-nisshi0 reactions
- Nov 1, 2025ci package conditional push to ghcr — nisshi-io/solace-nisshi0 reactions
On Bluesky · 10
- Oct 30, 2024it's not like-for-like but github.com/tansu-io/tansu is similar — @rmoff.net8 likes and reposts
- Sep 17, 2024Still wrapping my head around this one. Sounds fun though! tansu-io/tansu: Tansu is an Apache Kafka API compatible broker written in async… — @rmoff.net7 likes and reposts
- Nov 8, 2025The problem with building that is that... it exists! github.com/tansu-io/tansu — @stanislavkozlovski.bsky.social6 likes and reposts · 1 reply
- Jan 4, 2026tansu-io / tansu: Apache Kafka® compatible broker with S3, PostgreSQL, SQLite, Apache Iceberg and Delta Lake ★1021… — @rusttrending.bsky.social5 likes and reposts
- Dec 12, 2024📢 Tansu 0.1.0 is a drop-in replacement for Apache Kafka with PostgreSQL, S3 or memory storage engines. • No broker replication or… — @tansu.io3 likes and reposts
5 more on Bluesky
- Nov 15, 2025Kafka compatible broker with S3, PostgreSQL, SQLite, Apache Iceberg and Delta Lake #apachekafka github.com/tansu-io/tansu — @igfasouza.bsky.social2 likes and reposts
- Dec 12, 2024📢 Tansu 0.1.0 is a drop-in replacement for Apache Kafka with PostgreSQL, S3 or memory storage engines. • 🆕 Transactions and Idempotent… — @tansu.io2 likes and reposts
- Jul 13, 2026TIL - immediate concern is how to benchmark this (their abstraction plus any underlying storage layer) vs Kafka w/ built-in storage layer.… — @pires.devsays it tested it1 likes and repost
- Feb 8, 2026Rust実装で軽量なApache Kafka互換実装。ストレージとしてPostgreSQL,SQLite,S3,memoryが使える。箪笥。 — @matsuu.bsky.social1 likes and repost
- Oct 8, 2024これは面白い。 An Apache Kafka compatible broker with PostgreSQL and S3 storage engines written in async 🚀 Rust 🦀 github.com/tansu-io/tansu — @yassan168.bsky.social1 likes and repost
On Hacker News · 3
- Sep 16, 2024Tansu: A Kafka Compatible Broker with a PostgreSQL Storage Engine4 points
- Jun 6, 2025Tansu: A Drop-In Replacement for Apache Kafka with PostgreSQL and S31 point
- Apr 10, 2025Show HN: Tansu – Kafka Compatible Broker Transforms Proto/Avro/JSON into Parquet1 point
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository4 other files it gives its agents.
- nisshi-sans-io/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- nisshi-sans-io/CLAUDE.mdCLAUDE.md
- cut-releaseSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

