Links
AI / Agents
Developers
Community
Contributing guide
Code of conduct
Project roadmap
Security policy
Full license Apache-2.0
Meta
Requires: Python >=3.11
Provides-Extra: cli, objectstore, s3, gcs, azure, icechunk, neon, postgres, iceberg, delta, lance, ducklake, dolt, all
Package Info
tether
Version control (fingerprints, pins, & forks) for heterogeneous datasets.
A dataset rarely lives in one place: an Icechunk repository, a directory of raw files, a Postgres database, an Iceberg table, and the code that produced them. tether tracks all of them from one git or jj repository. Each commit records every object’s exact state – and, where the system allows it, pins that state with a native ref so it stays readable, and forks a writable branch off it when you want to change things without touching main.
Think of it as DVC for branchable systems: like DVC it commits small manifests to your repository, but where DVC only fingerprints files, tether also pins and forks live systems.
Status: pre-release (
0.1.0betas). ROADMAP.md says what has run against real services, what is still experimental, and what blocks 0.1.0; the changelog says what changed. Vibe coded with Claude Fable 5.1. USE AT YOUR OWN RISK.
Install
pip install "tether-vcs[cli]" # core + CLI
pip install "tether-vcs[cli,icechunk,lance]" # add the backends you use
pip install "tether-vcs[all]" # everythingThe distribution is tether-vcs; the package you import and the command you run are both tether. Python 3.11 or newer; git 2.38+ and/or jj 0.43+ on PATH (see the CLI guide). Extras: objectstore (S3/GCS/Azure for file), icechunk, neon, iceberg, delta, lance, ducklake, dolt, postgres, all.
Windows: commands that write are refused, since the locks need fcntl; status, verify, diff, log, ops and gc --dry-run work, and a default open is read-only. Use WSL to write.
Quickstart
tether lives inside a git or jj repository. Nothing below needs a cloud account: point it at an Icechunk repository and a directory you have on disk.
jj git init my-dataset && cd my-dataset # or: git init my-dataset
tether init
tether add zarr/imaging --kind icechunk ../data/imaging.icechunk
tether add raw/plate1 --kind file ../data/raw/plate1/
tether status # new / clean / modified per object
tether commit -m "Baseline" # pin the Icechunk snapshot; record the directory; jj/git commit pinned zarr/imaging -> tether.4e9503fc.73e341e966c83d38
recorded raw/plate1 (not recoverable)
committed b55326ca55a0
That commit id names the exact state of every object, forever: tether open zarr/imaging --rev b55326ca55a0 opens the pinned snapshot read-only, and TETHER_REV=b55326ca55a0 python report.py makes every repo.open() in a script do the same.
To change data without touching main, work on a bookmark:
tether new -b relabel # one branch per system, created on first write
python relabel.py # writes through repo.open("zarr/imaging")
tether commit -m "Relabel plate1" # pin the branch head; move the bookmark
tether promote # fast-forward main in every system, then move the main bookmark
tether verify --all-history # every pin any commit ever named still resolvesfrom tether import Repo
repo = Repo.find(".")
h = repo.open("zarr/imaging") # writable IcechunkHandle on this bookmark's branch
ro = repo.open("zarr/imaging", rev="main") # read-only at main's pinThe commands that plan before they write – commit, new, restore, promote, gc, drop, forget-workspace, import, repair, upgrade – take --dry-run (and --plan FILE / --from-plan FILE) so the store writes can be reviewed first. The Getting Started guide runs this walkthrough with full output; the user guide takes it from there.
How it works
- A dataset is a git/jj repository with one small manifest per object. History, branching, workspaces, and sharing are the VCS’s.
- Commits hold pins. Each dataset commit records, per object, an exact state and – where the system allows – a pin: a native, GC-proof ref (an Icechunk tag, a Neon branch, a git tag) that holds that state.
tether open KEY --rev Creads it back. - Bookmarks hold branches. The trunk bookmark (
main) stands for every object’s upstream branch; working on it writes there. Any other bookmark stands for one branch per system,tether.ws.<dataset>.<bookmark>, forked from the pins where it started and created on the first write. - Three verbs. commit pins the bookmark’s branch heads and moves the bookmark. promote moves each system’s upstream branch to those heads, then moves the
mainbookmark.pullreads what upstream has now and commits it. - tether is never in the data path. open returns the system’s native handle – an Icechunk session, a Postgres URL, a
DeltaTable– and steps aside.
What the VCS cannot see – what tether did to the stores – is in tether’s own operation log (tether ops, tether undo, tether repair).
What each backend can do
Every backend is fingerprinted; the rest depends on what the system offers.
| Backend | Recover | Pin | Fork | Promote | Merge | Diff | History |
|---|---|---|---|---|---|---|---|
| local files and directories; object-store prefixes (S3, GCS, Azure) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| single object-store objects with versioning enabled | 🟡 | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| git / jj repositories | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Icechunk repositories | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Lance datasets | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Delta Lake tables | 🟡 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Neon Postgres databases (experimental) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Apache Iceberg tables (experimental) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| DuckLake catalogs (experimental) | 🟡 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Dolt databases (experimental) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Recover: can a committed state be opened again later? ✅ held by a pin; 🟡 re-openable by version id for as long as the system keeps that version; ❌ tether can only tell you it changed. Pin: a durable native ref created at commit time. Fork: a writable branch off a pin for tether new. Promote / Merge: fast-forward, or three-way merge, the upstream branch to the fork. Diff: tether diff --content describes what changed inside the object. History: tether log lists the object’s own snapshots. Experimental backends have run against a fake of the service, not the service itself. Stable ones run their full lifecycle in CI; for file and Icechunk that means local storage, and their S3, GCS and Azure paths have not run against a cloud service yet (see the roadmap). The full matrix – state fields, flags, per-backend caveats – is in the backends guide.
jj or tether?
A tether command exists where an operation has two halves – one in the VCS, one in the stores – that must happen together: commit, new, pull, promote, restore, abandon, drop, forget-workspace, and undo / repair for what tether itself did. Everything that only touches files and history – describe, squash, rebase, push – is the VCS’s, and tether notices what it needs to: a bookmark deleted, renamed, or moved by hand shows up in status with what to do. The concepts guide has the table.
Non-goals
- Sitting in the data path: open hands back the system’s native handle and steps aside.
- Running between commands: no daemon, no watcher; states are compared when a command asks.
- Reimplementing history, branching, or sharing: that is the VCS’s job.
tether ops/undo/repaircover only what tether did to the stores. - Cross-system transactions or a query layer.
Documentation
https://rosebud-biosciences.github.io/tether/ – getting started, concepts, worked examples, CLI guide, backends, sharing and CI, troubleshooting, and the generated API and CLI reference.
Why this exists
Every existing tool versions a single layer:
| Tool | Scope | Relationship to tether |
|---|---|---|
| DVC (lakeFS-owned) | files/objects in git | closest structural analog; no pin/fork of live systems |
| lakeFS, Quilt, Oxen, DataChain | objects / files | analog of our file objects |
| Dolt, pgGit, Neon, Databricks Lakebase | one database | vendor-bound; no external-object pins; Dolt and Neon are backends |
| Nessie, Bauplan | Iceberg catalog branching | Iceberg-only |
| Icechunk, Lance, Delta, DuckLake | one dataset / table / catalog | we use them as backends |
| Yggdrasil | cross-system snapshot / branch / merge over Git, ZFS, IPFS, Iceberg, lakeFS, Dolt, … | the closest conceptual sibling; a JVM library, not adoptable from Python |
| Dagster observable assets | staleness detection | analog of our snapshot/drift step; no branching |
Nothing provides unified version control across files + Icechunk + Postgres + Iceberg with pinning and forking. So tether borrows jj’s working-copy and operation-log ideas (new REV, stale-working-copy detection, undo), DVC’s manifests-in-VCS layout, and Yggdrasil’s observe-then-record shape: a workspace that watches independent systems and records their snapshots, rather than a store that holds the data.
Development
uv sync --all-extras
uv run ruff format --check . && uv run ruff check .
uv run ty check
uv run pytest -n autoThe publish / import tests start an ephemeral PostgreSQL cluster through pytest-postgresql; they need pg_ctl on PATH or a Homebrew / Debian install (brew install postgresql@16; GitHub’s Ubuntu runners ship it) and skip otherwise. The use-cases story (tests/test_use_cases.py) regenerates the guide’s command output with TETHER_UPDATE_DOCS=1.
CONTRIBUTING.md has the gates a pull request must pass. Report vulnerabilities privately (SECURITY.md).
License
Apache-2.0, copyright Rosebud Biosciences, Inc. See LICENSE and NOTICE.

