Contributing
Dassie is open source, and its code, its specification and these docs all live in one repository on GitHub. This page gets you from a fresh clone to a change that builds, passes its tests and keeps the spec in step.
On this page
Before you start
Dassie is a Rust workspace with a TypeScript side: the node, the
dassie command and the desktop app are Rust; the wallet,
the dev environment, the website and the tooling are TypeScript on
Node. The
architecture page shows every crate,
app and package and how they connect, and the repository's
README
lists them by path.
Read the spec for the part you want to change before you change it: the overview, engineering, and the area docs that the index names for its paths. They say what the code cannot: what the part is for, what must stay true, and why. Specification explains how it is laid out.
Questions are welcome on Discord at any point.
Set up your environment
The easiest way to get every tool at the right version is Nix, with flakes turned on. The repository's flake has a development shell with all of them:
$git clone https://github.com/justmoon/dassie.git
$cd dassie
$nix develop
$pnpm install
The shell holds the Rust toolchain that
rust-toolchain.toml pins, with clippy, rustfmt and the
WebAssembly target; the wasm-bindgen CLI; Node and pnpm;
Chromium for the browser tests; Pebble and systemd's tools for the
end-to-end tests; mkcert for local certificates; minisign; and the
libraries the desktop app links. pnpm install installs
the npm and JSR packages exactly as pnpm-lock.yaml pins
them.
Without Nix
You need, on Linux:
-
the Rust toolchain named in
rust-toolchain.toml, with thewasm32-unknown-unknowntarget; -
the
wasm-bindgenCLI, at the version inCargo.lock; - Node 26 and pnpm 11, and Chrome or Chromium;
-
for the desktop app, pkg-config, GTK 3, WebKitGTK 4.1, libsoup 3
and libayatana-appindicator
(
nix/desktop-libs.nixlists them); -
for the end-to-end tests,
pebble,pebble-challtestsrv,systemd-socket-activateandmkcert. Without them, those tests are skipped on your machine. In CI they fail instead, so a missing tool never passes there quietly.
Why it works this way:
Nix for CI and releases, plain cargo and
node --run on your machine.
Build
The Rust side builds with plain cargo. The TypeScript side
has a script for each part, run with node --run:
$cargo build
$node --run build:wasm # the WebAssembly modules and their bindings
$node --run build:wallet # the wallet, into apps/wallet/dist
$node --run build:dashboard # the dev dashboard
$node --run build:website # dassie.land, into apps/website/dist
$node --run build:e2e # target/e2e/dassie, with the wallet built in
Build the WebAssembly modules before the wallet and the website,
which use them, and the wallet before build:e2e, which
builds it into the binary. The release builds, static binaries for
x86-64 and arm64, come from the flake:
nix build .#dassie-x64 or
nix build .#dassie-arm64.
To see your change run, node --run dev starts a small
network of nodes on your machine with a dashboard to watch it, and
rebuilds as you edit.
Local dev network shows how to
use it.
Run the tests and checks
$cargo fmt --check
$cargo clippy --all-targets -- -D warnings
$cargo test
$node --run check # formatting (dprint), lint (oxlint), types (tsc), the RPC contract (buf)
$node --run test # the apps', packages' and scripts' tests
$node --run test:e2e # WebAssembly vectors, STREAM interop, browser, end to end
node --run ci runs every Node step in order, building
what each needs first. With Nix, nix flake check runs
what CI runs, Rust and TypeScript alike, in the Nix sandbox.
Two more need something extra. The updater's tests need the test
release key built in:
cargo test -p dassie-cli --features test-key. If you
change a contract in proto/, run
node --run gen in packages/rpc; a test fails
until the generated code matches.
What each kind of test is for
- Unit and state-machine tests sit beside the code they test, in every crate and package.
-
Golden vectors in
testdata/pin the wire formats and key derivations that must never change. -
Interop tests in
tests/interopcheck STREAM against the reference implementation. -
End-to-end tests in
tests/e2erun the real binaries: an install, the wallet in a browser, payments over BTP and Open Payments, settlement over hubs on a fake XRP Ledger, swaps between peers and the market maker. None of them touches the public testnet. - The install tests run the installer and the updater on several Linux distributions in containers. They run in the release workflow, before a release is signed, and not on pull requests; the repository's README shows how to run them yourself.
CI runs every test on fixed seeds, so a random seed never fails a pull request on a bug it did not touch.
Simulations
The core crates are deterministic: they never read a clock or the
operating system's randomness, and every table and queue is bounded.
That lets dassie-sim run whole networks of nodes from a
seed, with crashes, partitions and lying peers, and replay any run
exactly. It is a tool for building a feature, not a test:
$cargo run --profile sim -p dassie-sim -- run peer 0..1000
$cargo run --profile sim -p dassie-sim -- explain peer 42
No test runs a simulation, in CI or on your machine. When a seed finds a bug, the fix comes with a targeted test that sets up that case directly. A campaign runs the simulations on the latest code from random seeds, outside CI, and files an issue for each new kind of failure.
Why it works this way: a simulation is noise to a repro, and costs every pull request its time.
How the code is written
The code follows a pragmatic version of TigerStyle, so that it can be simulated and trusted with money:
- Deterministic. Protocol crates take the time and their randomness as inputs. The same inputs give the same outputs.
- Bounded. Every queue, table, buffer and loop has a limit, named with its unit. Going past it is an error value.
- Asserted. Invariants are checked, in release builds too, and untrusted input gets an error value, never a panic.
-
Simple. Short functions, no recursion, no
unwrapoutside tests, units in names such as_msand_bytes.
TypeScript is written so that Node can run it by stripping its types:
no enums, no parameter properties, imports with .ts
extensions. The type check, part of node --run check,
enforces most of this;
Engineering
has the rest.
Keep the spec current
A change to Dassie and the change to its spec go together, in the same pull request:
- A change to what a part guarantees, or why updates that part's area doc. A changed value or list does not touch the spec: those live only in the code.
-
A choice between real alternatives gets a decision
in
spec/decisions/, with the facts, what was chosen, what it costs and what was rejected. Adding or changing a decision says how. - Work you do not build yet becomes an open area in the roadmap, never a numbered milestone.
- A new crate, app, package or contract gets a row in the spec's index.
Cite the spec by path and heading, as
spec/areas/peer-protocol.md#versions, and a decision by
its file. Never write history into it: no dates, milestones or pull
request numbers. This test checks every link, citation and the
index:
$node --test scripts/spec_test.ts
Keep the architecture page current
The architecture page draws everything
from one file,
apps/website/src/architecture/map.json.
Update it with your change:
-
A crate, app, package or contract is added, removed or
renamed: add, remove or rename its box in
components, with its path, a short label, its subsystem, a place in its layer, what it is and what it does, and its edges. Keep boxes apart and inside their layer. -
What a part does, or what it builds on, changes:
update its description and edges, and any journey in
flowsthat passes through it.
Write for someone new to Dassie, in the present tense. The test fails when a box is missing, a path is wrong, boxes overlap, or a box's links differ from its real dependencies:
$node --test apps/website/src/architecture/map_test.ts
Keep these docs current
These pages are hand-written HTML in
apps/website/docs/,
one index.html per page. The list of pages, in the order
the sidebar shows them, is
apps/website/src/docs.ts;
the build adds the header, sidebar, breadcrumbs and search. If your
change alters a command, a flag, a variable or what a node does,
update the pages that describe it. They describe what Dassie does
today, in Kopje's voice (calm, plain, present tense), and say "not
there yet" for what is not built. This test checks every page, its
links and the reference pages against the code:
$node --test apps/website/docs_test.ts
node --run dev in apps/website rebuilds the
site as you edit, and node --run preview beside it serves
it.
Before you send a change
- The Rust and Node checks above pass, or
nix flake checkdoes. - New behavior has a test, and a bug fix has the test that would have caught it.
- The spec, the architecture page and these docs say what the code now does.
Send a pull request
How we take pull requests from outside the project is not written down yet.
License
Dassie is licensed under the Apache License 2.0. The full text is in
LICENSE.