ADR-0026: First-party extension seam (amends ADR-0002)
Status: Accepted
Date: 2026-07
Amends: ADR-0002 — CLI Product vs Library
Amended by: ADR-0039 — The Scheduler Contract (2026-10-06, see Amendment below)
Context: A private, source-available companion crate — rivet-pro (BSL 1.1, separate repo) — now builds the paid tier (whole-database discovery, continuous CDC) on top of the OSS engine. It links the rivet library and depends on a small set of already-pub items. ADR-0002 declares the library “not a stable public API” and (Consequence #4 / Future library path) prescribes extracting a separate rivet-engine crate if a stable embedding surface is ever needed. This ADR decides what to do now that there is exactly one, first-party embedding consumer.
Decision
Name a minimal, stability-tracked first-party extension seam. Defer the full rivet-engine extraction until a non-first-party (external) consumer appears.
The seam is the exact set of library items rivet-pro depends on. It stays pub, and a change to the shape of any item below is a deliberate act: update rivet-pro in lockstep and record it in CHANGELOG.md under a Breaking (extension seam) line.
The seam (v0.16.x)
| Item | Path |
|---|---|
TypeMapping, RivetType, TimeUnit, TypeFidelity, SourceColumn | rivet::types |
ExportTarget (+ variants DuckDb/BigQuery/Snowflake/ClickHouse) | rivet::types::target |
ExportTarget::resolve_table(&[TypeMapping]) -> Vec<TargetColumnSpec> | rivet::types::target |
ExportTarget::resolve_column(TargetInput) -> TargetColumnSpec | rivet::types::target |
TargetColumnSpec, TargetInput, TargetStatus | rivet::types::target |
AdcUserTokenLoader, try_authorized_user_loader() | rivet::google_auth |
The ADC loader is on the seam for a reason worth stating: reqsign’s token
loader resolves service account → impersonated → external account → VM
metadata and has no authorized_user arm, so any native Google client
must supply one or it authenticates in CI and fails on a developer laptop.
rivet-pro wrote a second copy of this exchange before the seam existed, and
got three things wrong that this implementation had already learned — it
surfaced the token endpoint’s error body (Google echoes the submitted
client_id/client_secret back in some failure modes), it kept the secrets
in un-zeroed Strings, and it pinned an invented lifetime when expires_in
was absent. Exposing the loader is what makes “never hand-roll a second auth
path” true across BOTH repos instead of only inside this one.
The loader was WIDENED (0.24.x) to mint from a service_account key file too —
the RFC 7523 jwt-bearer grant, RS256-signed in process — so a consumer pointing
GOOGLE_APPLICATION_CREDENTIALS at a key file gets its token from this seam
rather than from a gcloud subprocess. The seam items are UNCHANGED
(AdcUserTokenLoader keeps its now-misleading name precisely because renaming
it to describe a widening would break a consumer for nothing); two additive
methods, credential_kind() and principal(), name the resolved identity for
logs. external_account / workload identity still returns Ok(None): it needs
an STS exchange rivet does not model, and a half-implemented subset would be
worse than the documented fallback.
Everything else in the library keeps the ADR-0002 posture: pub only for the test harness, no stability guarantee.
The CLI superset uses the process boundary, not a Rust API
rivet-pro ships a superset rivet binary (OSS subcommands + discover/daemon). It composes them at the argv/process boundary: unknown subcommands are delegated to the OSS rivet binary (already a stable product contract — config YAML + exit-code taxonomy + manifest). The cli module is deliberately binary-only (declared in main.rs, absent from lib.rs; its dispatch reaches crate::init, also binary-only). Pulling cli into the library to expose Commands/dispatch would violate ADR-0002’s minimal-library principle and freeze the entire command surface. We do not do that.
Amendment (2026-10-06) — the load and compact operators, orchestration and alerting are open source
Until this amendment the Context listed the warehouse load in the paid tier and the paragraph above listed load among the subcommands the superset binary adds. Both lists were corrected in place, because rivet load and rivet compact are subcommands of the OSS binary (Commands::Load, src/cli/args.rs:196; Commands::Compact, src/cli/args.rs:231), dispatched to load::orchestrate::run_loads and load::compact::run_compacts (src/cli/dispatch.rs:110-129), with BigQuery, Snowflake and ClickHouse as targets (LoadTargetKind, src/config/load.rs:135-139). Nothing else in those lists is changed by this amendment.
What it decides: the scheduler operators for load and compact, and the orchestration and alerting integrations as a whole, are open source and live in this repository — the operators for run / apply, load and compact, their DAG builders, and the notification formatters. They drive the OSS binary at the same argv/process boundary this ADR already names as the product contract, now specified by ADR-0039. An operator for an OSS subcommand that was itself paid would make the OSS command unusable under a scheduler without the private crate, which is the dependency direction the Rationale below forbids.
The seam table and its enforcement are unchanged.
Rationale
- One consumer ≠ a public API. A full
rivet-engineextraction (separate crate, two manifests, re-export churn) is the right move for external consumers with independent release cadence. With a single first-party consumer we own both sides, so a named + tested subset delivers the stability guarantee at a fraction of the cost. - Smallest seam. Every
pubitem on the seam is a compatibility commitment. The list is exactly whatrivet-prouses today — resolution only.Destinationpromotion (a likely next seam) is deferred until a Pro feature needs it, then added here with the same discipline. - One-way dependency.
rivet-prodepends onrivet; the OSS tree must never depend onrivet-pro— otherwise the MIT binary can’t build without the private crate and paid code leaks into an MIT distribution.
Enforcement
- Seam-stability canary —
tests/offline/extension_seam.rscompile-locks every signature above. If it fails to build, an item on the seam changed: updaterivet-proand add theBreaking (extension seam)CHANGELOG line — do not just edit the test to compile. - Dependency-direction guard — the CI
boundaryjob fails ifsrc/orCargo.tomlreferencesrivet-pro/rivet_pro.
Consequences
- The seam table is the contract. Widen it only by adding a row here + a canary line, never silently.
rivet-pro’s own CI (dual-checkout, builds against OSSmain) is a second, cross-repo canary.- Trigger to revisit: the day a second, external consumer wants to embed the engine, extract
rivet-engineper ADR-0002’s Future library path and move this seam into its semver’d surface.