Files
radicle-desktop/docs/adr/0002-backend-architecture.md
T
7a0b67da64 Add release COB ports and commands
- Add release types and the Releases and ReleasesMut ports
- Create, delete and redact releases
- Register, attest and locate artifacts
- Filter releases with radicle-artifact
- Add release commands and mirror them in test API

Co-authored-by: Brandon Oxendine <brandon.oxendine@gmail.com>
Co-authored-by: Rūdolfs Ošiņš <rudolfs@osins.org>
2026-10-08 18:20:59 +02:00

3.3 KiB

2. Backend architecture

Date: 2026-10-01

Status

Accepted

Context

The app needs the same backend behaviour in two places: inside Tauri for the desktop app, and over HTTP so the frontend can run in a browser for E2E tests and development without the Tauri runtime. If each runtime held its own logic, the two would drift and the E2E tests would stop exercising the code users run.

Most reads and writes go straight through the radicle crate. Some list views can't: reading patches, issues and notifications through heartwood's caches loads and sorts every item in memory on each page, which is too slow for large repositories.

This ADR records a design that grew from 2024 onwards (the HTTP driver in October 2024, the first domain service for the inbox in January 2025).

Decision

The backend follows a hexagonal architecture split across three crates:

Crate Role
radicle-types Domain types, ports, adapters and the ts-rs bindings
radicle-tauri Tauri driver: thin IPC wrappers around port methods
test-http-api Axum driver: the same ports exposed as HTTP routes

Both drivers depend on radicle-types and share no code with each other.

Ports with default implementations. Each port is a trait in radicle-types/src/traits/ whose methods have full default implementations built on Profile::profile(). A driver gets the whole behaviour by implementing Profile and declaring empty impl blocks, so AppState (Tauri) and Context (HTTP) behave identically by construction. Business logic goes in these defaults, never in a command handler or route.

Domain services for hot reads. Where going through radicle is too slow, a module in radicle-types/src/domain/ defines its own storage port and a generic Service over it. The Sqlite adapter in src/outbound/ implements those ports by querying Radicle's COB cache and notifications database directly (read-only). Both drivers construct the services at startup and use them for the matching list endpoints; everything else keeps going through the ports. Releases are the exception: radicle-artifact keeps its own SQLite cache of release COBs, so list_releases reads through that instead of a domain service.

Parity is enforced. Every Tauri command has a matching HTTP route. cargo test -p test-http-api --test parity fails when a command, route or frontend invoke has no counterpart, unless the command is listed in TAURI_ONLY with a reason.

Moving away from radicle-surf. Repository browsing (trees, commit history, diff structure) still goes through radicle-surf, and we intend to replace it with our own code in radicle-types. Surf may not be maintained in the long run, and we want finer control over performance and features: the slow paths (diff stats, opening files, commit counts) already bypass it with the git binary or git2. Owning this code behind a narrow interface would also let us swap the git backend later, for example to gitoxide. Surf is read-only and does no verification; trust comes from the signed refs that radicle resolves before surf reads anything, so the replacement doesn't have to preserve any security property. This is not scheduled yet.