Branch, diff, and merge for database schemas — the same workflow Git gives
code, applied to schema structure (tables, columns, constraints, indexes),
not text. Row data is explicitly out of scope; see decisions.md's Brief
section for the full problem framing, the hard part, and what was
deliberately cut.
The core idea: every schema object gets a stable ID that survives renames
and retypes, so a rename on one branch and a reference to that object on
another branch never look like a false conflict — and the one place a
conflict can still hide (free-text check-constraint expressions) gets
explicitly detected and explained, not silently merged into a broken
schema. That's the hard sub-problem this project goes deep on; see
merge-design.md and the merge-related entries in decisions.md.
Live: driftless-mc4m.onrender.com
(Render, free tier — the first request after a period of inactivity may take
a few seconds to wake the instance). A main branch is already imported
(the same users/email_format schema the flagship scenario below uses),
so you can jump straight to forking it rather than pasting DDL first.
Ask for a token to sign in with, or run it locally (below) with your own.
crates/domain schema model, apply_operation, diff, merge — dialect-agnostic
crates/adapters Postgres DDL parser/codegen (sqlparser-rs)
crates/persistence Postgres-backed branch/version storage (sqlx)
crates/api axum REST API over the above
frontend/ React + TypeScript UI (Vite) against the REST API
See docs/technical-backend.md / docs/technical-frontend.md for the full
current status of each layer, and decisions.md for every real decision
made while building, in the order they were made.
- Rust (stable) — rustup.rs
- Node.js 22+ and npm
- Postgres 16+ (CI runs against 16; a local instance you can create a database in)
1. Create a database. Any name/connection works — the app runs its own migrations on startup. Example, assuming a local Postgres reachable via a Unix socket:
createdb driftless_dev(Adjust for your setup — a postgresql://user:pass@localhost:5432/dbname
style URL works the same way. See crates/persistence's tests for both
socket- and TCP-style connection string examples.)
2. Set up your local environment. Copy the template and fill in real
values — .env is gitignored, so this only needs doing once:
cp .env.example .envDATABASE_URL and API_TOKENS are required (the process refuses to start
without them); BIND_ADDR/RATE_LIMIT_PER_MINUTE/DATABASE_POOL_SIZE are
optional and already commented out at their defaults in the template. See
.env.example for the exact format of each.
3. Start the backend:
cargo run -p api.env is loaded automatically on startup (via dotenvy) — no need to
export anything on the shell first, or re-set it every time you restart.
Listens on 127.0.0.1:3000 by default. Migrations run automatically on
startup.
4. Start the frontend, in a separate terminal:
cd frontend
npm install
npm run devOpens on http://localhost:5173. Vite's dev server proxies /api/* to
the backend (see frontend/vite.config.ts) so the browser never talks to
port 3000 directly — no CORS setup needed for local dev.
5. Sign in. The first screen you'll see is a token gate — paste in any
one of the identity:token:role entries' token segment from your .env's
API_TOKENS (e.g. dev_token_123, if you used the value from
.env.example unchanged). There's no signup flow; tokens are pre-issued,
matching this project's trusted-internal-team model (see decisions.md).
The UI has four tabs: Branches, Develop, Diff, Merge. To see the project's actual hard part (a rename that only breaks a check constraint's free-text expression, not its ID-based reference) end to end:
- Branches tab → Import DDL as a new branch, e.g.:
name it
CREATE TABLE users ( id INTEGER NOT NULL, email TEXT NOT NULL, CONSTRAINT email_format CHECK (email LIKE '%@%') );
main. - Fork a branch from
main, e.g.feature. - On
feature, use the Develop tab's Builder form to renameemail→contact_email(pick the column, choose "Rename," enter the new name, "Add to queue," then "Apply to branch"). Seedocs/product-guide.mdfor a screenshot walkthrough, orapi-reference.mdfor the underlyingPOST /branches/{name}/operationsrequest shape if you'd rather drive it directly. - Merge tab → preview a merge of
main(ours) andfeature(theirs). You'll see a Rename vs. expression reference conflict: the constraint's ID-based reference is fine, but its expression text (email LIKE '%@%') still says the old name. - Apply the suggested fix, watch the conflict resolve, and commit.
- Structured logs:
tracingthroughout,RUST_LOG-controlled (e.g.RUST_LOG=info,api=debug,persistence=debug). Every HTTP request gets atower-http-generated span (method, URI, status, latency); everypersistence::StoreDB call logs its own latency nested inside whichever request span triggered it, so a slow endpoint and the specific query behind it show up together in one log stream.
Logging is the only observability surface in the current code — no metrics
endpoint exists (an earlier Prometheus-format GET /metrics was removed;
see docs/technical-backend.md's Observability section for the full note).
cargo test --workspace # backend — 83+ tests; some are gated on
# DATABASE_URL and skip gracefully without it
cd frontend && npm test # frontend — 79 tests, no backend neededStart here:
decisions.md— the problem brief and every real decision made, in order, with alternatives and what was cut.docs/product-guide.md— a screenshot-driven walkthrough of the product from a user's point of view, including the flagship conflict scenario.
Design specs (written before implementation):
schema-model-design.md/diff-design.md/merge-design.md— the domain layer's design specs, including the full conflict taxonomy.
Technical reference (current, generated/verified against the actual code):
docs/technical-backend.md/docs/technical-frontend.md— architecture, extensibility analysis, and current status of every layer.docs/architecture-diagram.md— the backend's request-flow diagram, with a full walkthrough of how to read it.api-reference.md— every REST endpoint (auth, rate limiting, error codes, request/response shapes forSchema/SchemaOperation/SchemaDiff/Conflict), generated fromcrates/api's actual routes.