Documentation · Overview

The fastest
line down the channel.

Thalweg carries records from streaming and batch sources into analytical and operational stores — with the whole path, from wire to table, memory- and type-safe in one language.

thal·weg  /ˈtɑːlveɡ/  — the line of fastest flow, the deepest continuous channel down a watercourse. In law, the boundary drawn along a river: the line that carries the authority.

TWG — short for Thalweg, and read as Throughput With Guarantees: move a great deal of data, and never lose any of it.

52
Crates
65
Decisions recorded
3
Record representations
100%
Pure Rust*

*Two documented exceptions, both opt-in and off by default. Every other path is pure Rust with no C dependency on the hot path.

01How it works

A record enters from a source — a broker, a file, an Arrow stream. It is decoded once into a shared columnar form, checked against its data contract, transformed by SQL if you want it to be, and written to one or more sinks. Along the way every record carries its origin, so if a destination falls behind it can be rebuilt from the durable copy rather than re-read from the source.

Streaming and batch are the same pipeline. A file source is simply a bounded one — it ends, where a broker does not — so batch ingestion runs through the identical decode, contract, transform, sink and dead-letter path. Not a parallel implementation that drifts, and not a separate tool: one binary, with the mode as a subcommand.

twg stream runs the daemon against brokers and streams.
twg batch runs a bounded ingest over files.
twg config secrets lists the secret references a configuration needs, for provisioning.
SOURCE Kafka · Pulsar files · Flight DECODE → Arrow once CONTRACT ODCS + DQ clean/reject TRANSFORM SQL DAG WASM · Flight SINKS Postgres Delta · files PRIMARY RAW durable · acks source offset raw-anchored recovery — rebuild a lagging sink without re-reading the source ONE RECORD'S JOURNEY left to right — source offset advances only once the primary-raw sink confirms the write is durable

Every box on the channel is a stage you can observe independently — its throughput, its memory, and whether it's healthy or building backpressure — at /health and /stats.

What travels with every record. Origin is not a side-channel — it rides with the record as reserved columns under a _twg_ prefix, so it survives every hop, including the Arrow Flight boundary where an out-of-band sidecar would be lost. Two families, and the name tells you which. _twg_source_* is what the broker handed us — topic, partition, offset (or a Pulsar message id), key, timestamp, headers and the rest — as one transport-neutral set, with a column left null where a given transport doesn't supply it. Bare _twg_* is what Thalweg itself stamps: _twg_ingest_ts when we read the record, _twg_emit_ts when we write it, and _twg_record_id, a deterministic content hash that anchors dedup and recovery.

A full provenance clock on every record. _twg_source_timestamp (when the source recorded it) → _twg_ingest_ts (when we read it) → _twg_emit_ts (when we wrote it) — event time through to processing time. The raw lane keeps the whole set; decoded outputs keep a configurable subset (offsets only, by default). The complete column list, with types and per-transport availability, is in the delivery plan; the naming convention is ADR-0058.

02What's supported

Thalweg is early-stage: the architecture is decided and the workspace is scaffolded, with the first capability — user-defined functions — now built and the rest following. The table below is honest about that. It shows where each capability stands today, what is built, and what is planned but not yet started — for example RabbitMQ, NATS, and other transports are planned, not built.

Built Design complete Scaffolded Partial Planned

Sources

CapabilityStatusNotes
Apache KafkaScaffoldedPure-Rust client; consumer groups incl. cooperative-sticky
Apache PulsarScaffoldedFresh binary-protocol client; staged subscription and memory-aware shedding designed in
Batch filesScaffoldedParquet, CSV, JSON, XML, Excel, Avro OCF — same binary, same pipeline as streaming
Arrow Flight (gRPC)ScaffoldedAlready-Arrow, zero decode
Arrow IPC (file & stream)ScaffoldedNon-Flight raw IPC
Delta Sharing (recipient)ScaffoldedCross-org read via pre-signed URLs
Arrow C Data InterfaceScaffoldedIn-process zero-copy handoff from PyArrow/Go/C++/Java
RabbitMQPlannedNot yet started
NATS / JetStreamPlannedNot yet started
AWS KinesisPlannedNot yet started
MQTTPlannedNot yet started
REST (poll / webhook)PlannedNoted for future consideration
WebSocket (WSS)PlannedNoted for future consideration

Payload formats

FormatStatusNotes
Avro (registry + OCF)ScaffoldedConfluent / Glue / Apicurio wire framing; OCF shares decode
ProtobufUnder reviewDecode strategy open: zero-copy descriptor-driven parser, gated on licence + benchmark
JSONScaffoldedSchema-driven + inferred
XMLScaffoldedquick-xml + bespoke XSD/contract-driven mapping
Custom binary (WASM)ScaffoldedBatch-oriented WASM decoder; native compile-in exception

Schema tooling

CapabilityStatusNotes
Protobuf source bundlingBuiltAny .proto source → one FileDescriptorSet + combined/per-package .proto + message manifest. See Command-line tools
Protobuf → ODCS contractBuiltProjects a descriptor set to an ODCS v3.1.0 data contract via a neutral schema hub
JSON Schema / Avro bridgePlannedFurther spokes on the same hub; leaf-type mappings already exist in twg-type-map
ODCS → Protobuf generationPlannedThe reverse direction; field numbers synthesised deterministically

Sinks & table formats

TargetStatusNotes
PostgreSQLScaffoldedBinary COPY; Postgres 18+; primary-raw eligible
Databricks (Zerobus)PartialArrow Flight → Delta. Fire-and-forget ack lane, stream pool, catalog provisioning, type reconciliation and primary-raw preconditions built and fake-tested; the real client is behind the databricks-sdk feature (Arrow-IPC bridge). Iceberg recovery read-back and live execution pending (ADR-0065)
Parquet (object store)ScaffoldedPlain files; primary-raw eligible, no catalog needed
Apache IcebergScaffoldedRead + write, catalog-mediated. The one open table format carried; reader feeds recovery
Arrow Flight / IPCScaffoldedFirst-class out-transports
DuckLakePlannedSource & sink; treated like other DB targets
REST / WebSocketPlannedPush/forward sink; noted for future consideration

Platform capabilities

CapabilityStatusNotes
Data contracts (ODCS)ScaffoldedOptional. Schema authority, quality rules merged tightest-wins, drop rules; loaded local/HTTP/object-store
DQ audit trailScaffoldedFull-grain per-record per-rule; durable sink; on by default
Raw-anchored recoveryDesign completeGap-aware coverage, bounded windowed replay, separate scale-to-zero role
Sink coverage storePartialGap-aware union-merge ranges + gap computation + in-memory backend built (the `CoverageStore` seam); durable colocated-with-raw backings and the write-behind cache pending
Content deduplicationDesign completeCatches upstream duplicates the record ID cannot; opt-in and contract-gated
Coordination & quorumDesign completeQuorum as a worker role; substrate deferred until recovery is distributed
SQL transforms (DataFusion)ScaffoldedDAG with interior sinks, materialised intermediates
User-defined functions (UDFs)BuiltLocal DataFusion scalar UDFs (JSON, salted-hash, Spark-compat, Avro decimal) plus a hardened remote Arrow Flight UDF path — API docs
Health & observabilityScaffoldedPer-stage health tree, OTLP, Prometheus, /stats
K8s / GitOps configScaffoldedVerbatim TOML delivery; secret enumeration
Pulsar flow controlDesign completeStaged subscription, topic-spread shedding, byte-budget admission, pressure-as-scale-signal
Java / Python facades (C ABI)Design completeReserved; deferred (design-for, don't-build)

03Design principles

These ideas shape every decision in Thalweg. They are worth reading before the architecture, because the rest follows from them.

Pure Rust, sans-io edges

No C-linked broker client on the hot path. Codec and auth crates carry no async runtime, so they are reusable anywhere — a Lambda, a query engine, a transport not yet written.

Three co-equal representations

Raw bytes, a neutral value model, and Apache Arrow — none privileged. Pass through untouched, decode per-record, or decode straight to columnar batches.

Everything is modular

Every source and sink is optional; a deployment wires only what it needs. Kafka and Pulsar implement the same Source/Sink interface, so flow control, health, retry and recovery are written once and shared across every transport.

Raw is the system of record

Any replayable sink can hold the primary-raw role. The source is acknowledged only once raw is durable; lagging sinks rebuild from raw, never by re-reading the source.

Every decision is written down

Fifty-nine architecture decision records, a scenario catalogue, and encoded regression tests — the hard-won lessons are permanent, not tribal knowledge.

Structural rules are mechanical

The layering, the single Arrow version, the confinement of unsafe code — all enforced in CI, so the architecture cannot erode silently under delivery pressure.

04The layers

Crates are grouped into layers with a strictly downward dependency direction, enforced in CI. Nothing in the codec layer may depend on a transport; nothing in the codec or SASL layer may depend on an async runtime.

COMPOSE kafka-arrow · pulsar-arrow · cli (twg) FORMATS parquet · iceberg SINKS postgres · zerobus · object-store PIPELINE stream-arrow · pipeline-sql · pipeline-wasm · pipeline-flight CONTRACTS contract-core · contract-odcs · dq-enforcer · dq-audit CODEC codec-core · type-map · avro · protobuf · json · xml · custom FRAMING schema-registry · table-catalog CONNECTOR connector-core SOURCES kafka-client · pulsar-client · source-batch · source-flight · … TRANSPORT wire-tls · wire-sasl · wire-compression (no runtime, no format)

Reading bottom to top: raw bytes come in through transport primitives, up through sources into the shared connector contract, get decoded and validated, transformed, and written out. The codec-core crate holds the single Arrow re-export — the one place the Arrow version is named, so the whole workspace pins one version.

05Crate reference

Filter by layer, or read straight through. Every crate is currently a scaffold — the descriptions are the contract each will fulfil.

06Contracts & data quality

A data contract describes what a stream is supposed to contain. Thalweg reads ODCS — the Open Data Contract Standard — and uses it to decide both the shape of the data and what counts as acceptable.

Contracts are optional. Without one, schemas are inferred or taken from a schema registry and quality rules come from configuration. With one, the contract is authoritative on schema, and its quality expectations merge with any configured manually.

schema authority

Where a contract defines schema it wins over inference. For CSV, JSON and XML that is also faster — inference samples records and guesses; a contract does not.

rules merge, tightest wins

A manual rule and a contract rule on the same field resolve to the more restrictive. Neither source can silently loosen a constraint the other set.

loaded from anywhere

Local file, HTTP, or object storage — validated at startup, so a missing or malformed contract fails before the first record rather than during.

three enforcement modes

Strict fails the batch, quarantine diverts bad records, annotate flags them and lets them through.

drop rules

Known junk — heartbeats, test traffic — is discarded deliberately and silently, so the dead-letter queue stays a signal about real failures rather than noise.

one dead-letter path

Decode failures, contract violations and quality failures all land in one place, tagged by reason. Dropped traffic never appears there.

audited

Every record's verdict on every rule is recorded durably, stamped with which rule version produced it — so a verdict stays meaningful after the rule changes.

gates deduplication

Content deduplication is only permitted where a contract declares a natural key, since dropping byte-identical payloads is unsafe without one.

07Recovery & guarantees

The correctness backbone is one rule, and it applies to every kind of position the system holds: position may lag, but must never lead. A source position ahead of what was durably written resumes past records that never landed; a sink marked as written when it wasn't is never replayed. Both are silent loss. Behind, in either case, is a redundant replay that deduplication absorbs.

So the source is acknowledged only once the primary-raw sink confirms durability, and coverage is recorded only after the sink it describes has acknowledged. Never before, never concurrently.

Why this matters to you. A slow or failed destination never blocks your source and never loses data. Once the record is durable in raw the source advances, and any lagging destination is rebuilt from raw by a bounded, deduplicated replay.

Recovery is not optional past one sink

Once the source is acknowledged, the broker moves on and can no longer tell you what a downstream destination missed. From that moment the coverage store is the only record of it. A multi-sink topology without recovery isn't less resilient — it's silently lossy by construction, so configuration refuses one.

gap-aware coverage

Covered ranges per destination, not a high-water mark. A sink writing batches 1, 2, 4, 5 while 3 fails leaves batch 3 identifiable; a watermark of 5 would lose it silently.

never over-claims

Coverage may lag behind reality, costing a replay. It may never claim a write that didn't happen, which would be permanent loss.

lives with raw

Never inside the destination it describes — a store in a failed sink can't record that the sink failed.

deterministic IDs

Each record hashes to a stable ID including its source cluster, so replay deduplicates and overlapping ranges merge harmlessly.

bounded windows

Replay runs in time- and row-bounded windows, so catch-up never runs memory away — and the bound is what lets several agents share the work.

a separate role

Recovery runs as its own deployment mode, so a large backfill can't throttle live ingest. It scales independently, to zero when nothing is behind.

unified DLQ

Decode, contract and quality failures land in one dead-letter path, tagged by reason — never mixed with deliberately dropped traffic.

local buffer

Coverage is buffered on the pod and checkpointed on an interval, so commits are fast and inspectable on the host rather than only through a remote store.

08Scaling & coordination

Workers scale freely on throughput. What they need to agree on is deliberately tiny — which recovery window each agent has claimed, which node drives recovery — so the coordination layer is sized for that rather than for the data path.

Quorum is a role, not a tier

Rather than a separate coordination deployment, a subset of workers additionally hold quorum. A node determines its roles when it starts: worker, recovery worker, and where the cluster is short of members, a quorum member too.

A node holding quorum lowers its own throughput budget. Consensus means an fsync per commit and replication to peers, which contends with ingest for the same disk and CPU — so it takes less work and leaves headroom, trading its throughput for the cluster's stability.

0 nodes — feature off 1 node — full function, no fault tolerance 2 nodes — refused 3+ — fault tolerant, odd sizes only

Two nodes is refused rather than discouraged: quorum of two means both must be up for any write, so losing either halts writes. That's strictly worse availability than a single node, which keeps serving alone — the one size worse than both its neighbours, and the one an autoscaler would step through by accident.

Membership changes far less often than pod count, which is what makes this workable: a three-member cluster tolerates one member gone, so scaling three workers to two needs no reconfiguration at all.

Catching duplicates the record ID can't

The record ID hashes source position, so broker redelivery is idempotent. It does nothing for a duplicate created upstream — a producer's own retry republishes the same record at a new offset, which hashes differently and passes through. Hashing the payload catches it.

Opt-in, and contract-gated. Two genuinely distinct events can have identical payloads — heartbeats, timestamp-less clicks. Content deduplication would drop the second, and that's data loss caused by the safety feature. So it's enabled per stream only where the data contract declares a natural key or uniqueness guarantee, and validation refuses rather than warns.

09Delivery plan

Phases, with the constraint that shapes them: nothing ships that would be silently lossy. Where a capability can't be made safe yet, configuration refuses it rather than allowing a topology that only looks correct.

PhaseDeliversGate
0 · FoundationsWorkspace, CI, structural gates, static musl build proven on both architecturesA deliberately failing commit is rejected
1 · Transport & value modelTLS, SASL, compression; the neutral value model and codec traitsValue model round-trips all four formats before any codec exists
2 · Avro & registrySchema registry, Avro across all three representationsByte-identical against a reference decoder
3 · Kafka consumerConnections, fetch, manual assignment. Passthrough ships hereSurvives broker restart and leader election
4 · Consumer groupsJoin/sync/heartbeat, cooperative-sticky rebalancing20-member group survives rolling restarts without duplicate assignment
5 · Sinks, coverage & recoverySinks and connector layer, then coverage and replay. Single-sink until recovery landsA failed sink's gaps are recorded and restored without touching the source
6 · ProducerIdempotent and transactional produceNo duplicate commits across coordinator failover
7 · BreadthProtobuf, JSON, custom-WASM decode, database sinks, catalogs, cloud auth (AWS/Glue block)Protobuf decodes byte-identical to a reference; a database sink survives restart
8 · PulsarFresh client with staged subscription and memory-aware sheddingCodec crates reused unmodified — the layering claim tested for real
9 · Transform & packagingSQL DAG, WASM UDFs, Flight sidecars, static musl and OCI imagesA three-level DAG with correct commit semantics across restart

Phase 5 is the one to watch: it carries the sink layer, the connector abstraction and the recovery machinery, and recovery is a precondition for the second sink rather than hardening applied afterwards.

10Command-line tools

Most of the engine is scaffold and design, gated behind the delivery plan. One capability is built and usable today: turning a Protobuf source tree into the artefacts the rest of the platform consumes, and projecting it to a data contract.

Status. built  twg proto bundle is the first fully-wired subcommand. It is pure Rust — it parses and resolves .proto syntax itself, with no protoc binary on the path.

twg proto bundle

Resolve any Protobuf source — a single .proto file, a bare directory of protos, or a Java/Maven/Gradle repository with many src/main/proto roots — into one combined descriptor set plus human-readable renderings and a machine-readable manifest.

# bundle a repository into every artefact (the default) twg proto bundle <SOURCE> --out-dir ./out # a repository that needs types from a second repository twg proto bundle <SOURCE> --out-dir ./out --name orders \ --include-root <SHARED_SOURCE> # point at a subdirectory: bundle just that subtree + its transitive deps # (cross-module imports are auto-resolved from the repo) twg proto bundle <REPO>/some/module/src/main/proto/pkg/sub --out-dir ./out # if the command reports a conflict, prune the divergent copy it names twg proto bundle <SOURCE> --out-dir ./out --exclude vendor # emit only the descriptor set and the ODCS contract twg proto bundle <SOURCE> --out-dir ./out --emit pb,odcs

OPTIONS

FlagDefaultEffect
<SOURCE>A .proto file, a directory of protos, or a repository root. Required.
--out-dirDirectory to write outputs into. Required.
--namesource stemBasename for the emitted files.
--emitpb,single,proto,manifest,odcsWhich artefacts to write, comma-separated. An unknown value fails fast.
--include-rootauto-detectAn extra include root added on top of auto-detection, for a dependency whose files live outside the source. Repeatable.
--dep-searchsource repoAn extra directory to search for dependency-providing src/main/proto roots — e.g. a separate repo holding shared contracts. By default the source's own repo (found via .git) is searched. Repeatable.
--no-auto-resolveoffReport missing dependencies (with the files that need them) instead of resolving them from the repo.
--excludenonePrune any subtree whose path contains this component — e.g. a vendored copy that diverges from the canonical tree. Repeatable. target, build, rust-build, .git and node_modules are always pruned.
--include-glob**/src/main/protoThe trailing path that marks an include root in repository mode.
--on-conflicterrorWhen one import path resolves to files with differing content: error (refuse, and name a subtree to --exclude) or first-wins (keep the first copy and warn). Prefer --exclude, or point the source at the single canonical subtree.
--wkt-importscentralWhere the Google well-known-type imports live in the rendered .proto: central (one shared base file) or per-file.

OUTPUTS

FileWhat it is
<name>.pbOne binary FileDescriptorSet — every file in the source plus the reachable Google well-known types. The authoritative, machine-consumable artefact.
<name>.protoA single combined .proto. Standalone-compilable when the source is one package; a package-delimited combined view (labelled as such) when it spans several.
<name>/<package>.protoOne valid proto3 file per package, importing its siblings.
<name>.messages.jsonEvery fully-qualified message type, with the entry/root types flagged — the strings you hand to the schema and decode paths.
<name>.odcs.jsonAn ODCS v3.1.0 data contract projected from the schema: one schema object per message, cross-references preserved as relationships.

any source shape

A single file, a bare directory, or a multi-root repository all resolve through one command; the layout is auto-detected.

duplicate-safe

The same import path under two roots is collapsed only when byte-identical; a divergent copy is refused, and the error names the subtree to --exclude to keep the canonical one.

deps auto-resolved

Point at a subdirectory and its cross-module imports are pulled in automatically from the repo (found via .git); the bundle stays focused on the target plus exactly its transitive deps.

contract projection

The same descriptor set is projected to an ODCS contract, the first spoke of a neutral schema hub that JSON Schema and Avro will join.

Reading the output. The summary prints a sample of the fully-qualified root/entry message types — the concrete message_type strings to feed the schema and decode paths — and points at <name>.messages.json for the full list. A multi-package source whose packages reference each other cyclically renders a combined-view .proto that will not recompile standalone; the .pb is the artefact to use there.

Design records: docs/adr/0063 (bundling) and docs/adr/0064 (the schema bridge). The other twg subcommands — stream, batch, config — are scaffolds pending their delivery-plan phases.

11Decision records

Every significant choice is recorded as an ADR, so the reasoning survives the people. One remains genuinely open, pending a trade study; one is drafted and accepted, and one newly drafted and in review; the rest are decided and awaiting drafting into full prose.

Open decision: protobuf decode benchmark. These are flagged as open rather than guessed at.

The full index lives in docs/adr/README.md. Each will be drafted in the house ADR style and synced to Confluence.

12Why this name

The thalweg is the line of fastest flow — the deepest continuous channel down a watercourse, the path water takes when it has a choice. For an engine whose job is the efficient path from source to destination, that is the thesis in one word.

It carries a second meaning that happens to describe a real design decision. In international law, the thalweg is the boundary drawn along a river: the line that carries the authority. That is precisely how this system treats its primary-raw lane — one sink holds the authoritative copy, the source is acknowledged against it, and everything else is reconciled back to it.

TWG is the consonant skeleton of T-ha-L-we-G, so it reads as short for Thalweg. It also expands to Throughput With Guarantees — and those guarantees are specific rather than decorative: the source advances only on a durable sink acknowledgement, deterministic record IDs make replay idempotent, raw-anchored recovery rebuilds lagging destinations, and data contracts enforce shape at the boundary.

The project was drafted under the working name Millrace. That name turned out to be published on crates.io already — by another streaming event system, a direct collision in this project's own domain — so it was changed before anything was pushed or published. Thalweg is also a watercourse term, which is why the channel diagrams, the sluice-gate stages, and the weathered-copper palette on this page all survived the rename intact.

What else was considered

Other water-engineering candidates were checked for availability and brand collisions. penstock had the best literal meaning — the pressurised conduit feeding a turbine — but collides with a healthcare vendor whose product is audit software, an unfortunate neighbour given this system's own audit trail. headrace collides with an HR-tech company. adit was free but reads as a mis-typing of "audit". sluice, weir, leat, grist, winnow, spillway, braid and riffle were all already published crates.

A bare three-letter acronym with no parent word was considered and rejected: the available combinations were either colliding — trc and drc are smart-pointer crates, tde is Transparent Data Encryption, avc is H.264 video — or characterless. Deriving the prefix from a real word gives both a memorable name and a short prefix. Within that, thw was rejected in favour of twg: it is the well-known abbreviation for Technisches Hilfswerk, Germany's federal civil-protection agency, an awkward adjacency for a German-named organisation.

Crates are prefixed twg-; the command is twg. twg proto bundle is built and usable today; twg stream, twg batch and twg config are scaffolds pending their delivery-plan phases.

13Document map

Where to find things in the repository.

You want…Look in
This overview & navigationdocs/site/index.html
Rendered ADRs, blueprints, ops & testingdocs.thalweg.dev/reference/
Per-crate API reference (rustdoc)docs.thalweg.dev/api/
Why a decision was madedocs/adr/
The delivery plan & phasesdocs/blueprints/
How config & GitOps workdocs/operations/CONFIG.md
Health & observability modeldocs/operations/HEALTH.md
Batch reader library choicesdocs/operations/BATCH-READERS.md
Test scenarios & regressionsdocs/testing/
Publishing, licensing, versioningdocs/PUBLISHING.md
How this site is deployeddocs/operations/DOCS-DEPLOY.md
Working here as an agentAGENTS.md
Logo, colours, asset filesdocs/site/brand/README.md
Repository map (graphify)interactive map · generated with graphify.com