This is not an educational example invented for the occasion. Each code snippet below comes from the Aurabase root Cargo.toml and its crate manifests, as they exist in the repository today — including two discrepancies that we found while re-reading them for this article, and which we are documenting as is rather than quietly fixing them before publication.
- The Aurabase root Cargo workspace declares 18 explicit members: 11 services, the
aura-cliCLI, 5 shared libs and theaurabase-rsSDK — underresolver = "2"and a singleCargo.lock. [workspace.dependencies]centralizes shared versions; each crate inherits with{ workspace = true }rather than setting its own number — except when a crate overrides, silently.- A folder under
services/is not automatically a member of the workspace: thememberslist is explicit, not a glob, precisely to be able to exclude non-Rust code. [profile.release]applies only once to the entire workspace — a choice likepanic = "unwind"then applies to all eleven services at once.
One depot per service, or just one Cargo.lock?
A workspace Cargo groups multiple crates under a single Cargo.lock and a single target directory — the very functionality that Rust's package manager provides for this case. Eleven separate Rust binaries, each in its own repository, seem more independent at first glance. In practice, this means eleven distinct Cargo.lock, eleven version resolutions that may diverge over time, and no guarantee that two services compile the same version ofaxum or sqlx.
A Cargo workspace solves this at the package manager level, not the team discipline level. All members share a single Cargo.lock at the root: a common dependency is resolved once, to an identical version, for the entire graph. This is also what makes a cross-service refactor (changing a signature in aura-core, for example) visible to a single cargo build --workspace, rather than discovered service by service in production. This is one of the choices that distinguishes our 100% unified Rust core from a heterogeneous stack assembled service by service.
The Cargo.toml root: resolver and members
It all starts with the declaration [workspace] and its list members. At Aurabase, this list is handwritten, grouped by role — services, tools, libs — rather than generated by a generic pattern like services/*.
resolver = "2" is not a cosmetic detail. Cargo's resolver v2 isolates the functionality of build-dependencies and target-specific dependencies (target.'cfg(windows)', for example) from the rest of the graph — they no longer leak into the final binary. It also unifies the versions of the same dependency across all members of the workspace who share it: a single axum, only once, not eleven independent resolutions.
[workspace.package] once declares the common fields — version, edition, authors, license — that each crate can inherit with version.workspace = true instead of copying them.
Aurabase's eleven services all follow this pattern. Two crates deviate, and this is the first of the two discrepancies found while preparing this article: aura-cli declares version = "0.2.0" in hard copy, and the lib libs/aura-migrations declares version = "0.1.0" — both different from the 0.1.1 of the workspace.
version.workspace = true is optional, field by field, crate by crate. Nothing prevents a crate from keeping its own numbering — voluntarily (a tool published separately, for example) or by forgetfulness. A workspace audit should check this field crate by crate, not assume inheritance.
workspace.dependencies: a source of truth, unless a crate bypasses it
[workspace.dependencies] centralizes dependencies shared by several crates. Each service refers to it with { workspace = true } instead of setting its own version constraint.
This is the second real discrepancy: the workspace centralizes governor in version 0.10, but aura-gateway redeclares its own line governor = "0.8" instead of inheriting — the gateway applies its rate limiting with the bare governor crate, when aura-auth, aura-ai, aura-db and aura-functions pass through tower_governor in middleware. A workspace does not prevent divergence: it only makes it visible, if we take the trouble to compare.
Not all dependencies deserve to be centralized, however. wasmtime does not appear anywhere in [workspace.dependencies]: only one crate, aura-functions, uses it for its WASM runtime, so it remains declared locally. The rule we apply: raise a dependency to the workspace level from the moment two or more crates share it, not before.
libs/ to services/: which depends on what
The five shared libs (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) are declared as path dependencies in [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — then each service chooses which ones it actually needs.
The resulting graph remains readable from one file to another. aura-gateway only depends on three of the five internal libs — aura-core, aura-crypto and aura-telemetry. Enough to route, sign a JWT for the PostgREST sidecar and export its traces, without touching aura-db-adapters nor aura-migrations. aura-provisioneradds aura-migrations: it replays the schema migrations resulting from the creation of a project.
The narrowest case is aura-migrator, the dedicated binary that performs CI/CD migrations. Among Aurabase internal libs, its production [dependencies] only listsaura-migrations — aura-core only appears in its [dev-dependencies], for testing. The binary delivered to production therefore does not include anyaura-core code; it only exists during cargo test.
One crate, several binaries: aura-realtime division
A workspace does not require creating a new member each time you want a new deployable process. aura-realtime remains a single member of the workspace, but its Cargo.toml declares three distinct [[bin]] tables around the same shared [lib].
The manifesto itself documents the boundary between the two. cdc-worker captures Postgres changes by polling and publishes to NATS in pub/sub core (Client::publish_with_headers), without ever opening a WebSocket server — JetStream, in this same service, exclusively serves the cross-instance presence KV, not the CDC fan-out. ws-front only consumes NATS and holds WebSocket/SSE connections, without ever touching the CDC — the full details are in thereal-time engine documentation. Both binaries share the same RLS filtering code via [lib], but deploy and scale independently in Kubernetes. This is the right signal to choose several binaries in a crate rather than a new workspace member: same internal logic, different deployment topologies.
A file under services/ is not necessarily a Cargo member
The aura-edge-runtime folder does exist in the Aurabase repository. However, it does not contain any Cargo.toml — only TypeScript (index.ts, envelope.ts), and it does not appear anywhere in the members list of the root workspace.
This is exactly why Aurabase's members list is handwritten rather than replaced by a generic pattern like services/*. A glob would have attempted to include this non-Rust folder in the workspace, with a resolution failure as a result. An explicit list allows folders that do not speak the same language to coexist, under the same parent services/.
The lesson generalizes: counting the services of a Rust backend by listing the subfolders of services/ gives a false number. Only the root Cargo.toml is authoritative on what actually compiles in the workspace.
[profile.release]: a single setting for the entire workspace
A [profile.release] declared at the root of the workspace applies to all members compiled in release mode — only one place to adjust, not eleven. Cargo documents all available keys in its compilation profile reference; Aurabase only activates five.
The choice of panic = "unwind" rather than "abort" is documented directly in the file comment: a panic in an axum handler is intercepted by tokio, the task returns a 500, and the process continues to serve other concurrent requests. The performance gain ofabort is not worth the loss of inter-query isolation.
Freeze the toolchain, and the commands that matter
A unified workspace sets dependency versions, but not the version of the compiler itself. rust-toolchain.toml, at the root, freezes the toolchain (channel = "1.93" today) for any direct call to cargo or rustc in the repository.
This file exists precisely because a silent drift occurred: a CI action installed the stable channel of the day without reading rust-toolchain.toml, while the release Docker image compiled with the frozen version. A commit could pass all the tests with today's stable Rust, then break at the image build — discovered after the merge, not before. rustuprespects this file for any direct command: it was the only necessary fix, without affecting CI workflows.
One last detail, for those who read a manifest before executing it: the aura-cli crate compiles a binary whose table [[bin]] names it aurabase, not aura. The published npm wrapper, @aurabase/cli, exposes the two commands — aura and aurabase both point to the same script. The name of a [[bin]] Cargo, the name of the crate, and the name exposed by an npm wrapper are three separate things; none can be guessed from the other two.
Checklist: where to declare what
Five decisions come up each time you add a crate to a Cargo workspace. Here is where each is declared, based on the Aurabase example covered above.
| List of members | [workspace] members | Root — explicit list, never a glob |
|---|---|---|
| Shared version/edition | [workspace.package] | Root — version.workspace = true, by crate, optional |
| Dependency shared by 2+ crates | [workspace.dependencies] | Root — then { workspace = true } in each crate |
| Dependence on a single consumer | [dependencies] of the crate | Locally, without going through the workspace |
| Build profile | [profile.release] | Root only — applies to all members |
To audit an existing Cargo workspace — yours or that of a project you are taking over — four checks are enough to find the kind of discrepancies documented above:
- Compare
[workspace.package].versionto theversionof each crate — a different value is not necessarily a bug, but is worth documenting. - Compare
[workspace.dependencies]to the dependencies declared locally by each crate — spot the names present on both sides with different versions. - Compare the
memberslist in the rootCargo.tomlto the actual subfolders in the repository — a folder missing frommembersis not necessarily an oversight. - Check the actual name of each compiled binary (
[[bin]] name) rather than assuming it matches the crate name or the name exposed by a possible npm wrapper.