navree

The crate hierarchy

The layers of Navree crates, the rule that a crate depends only on the layers below it, the shared contracts that make that possible, and the test that enforces it.

On this page

Introduction

Navree isn't one big crate. It's a workspace of small crates, one per module, arranged in layers from L0 (the kernel and the macros) to L6 (the navree facade and the CLI), with applications on top. A crate may depend only on crates in lower layers, and only on the ones its layer is allowed to use. A test checks every dependency edge in the workspace each time the tests run.

This keeps the framework honest: the kernel knows nothing about HTTP, HTTP knows nothing about databases, and authentication builds on everything else rather than the other way round. You can read, test or use any crate with only what's below it. Most apps never see the split, because they depend on the facade, but it explains why things are where they are.

The layers

Text
LayerCrateWhat it provides
L0navree-kernelModule graph, type-keyed DI container, lifecycle, command runner
L0navree-macros#[module], #[injectable], #[controller], route and access attributes
L1navree-contractsShared traits and types: Secret<T>, RequestId, Principal, AuthState, sinks, Broadcaster, jobs, Clock
L1navree-configTyped configuration from .env, TOML files and APP_* variables
L2navree-httpAxum integration, default Tower layers, NestJS-format errors, controllers, rate limits, trusted proxies
L2navree-logtracing subscriber, JSON or pretty output, redaction, rotating files
L2navree-cryptoAES-256-GCM encrypter with key rotation, Argon2id hasher, token helpers
L2navree-eventsTyped event bus with sync and queued listeners
L2navree-viewMiniJinja templates (mail and other text)
L2navree-cacheCache stores (memory on moka), remember, tags, locks
L2navree-http-clientOutgoing HTTP with retries, timeouts, an SSRF policy and fakes
L3navree-dbSeaORM integration, per-request unit of work, migrations, seeders, factories, pagination
L3navree-testingTestApp, an in-process TestClient, fakes
L3navree-observeHealth checks, readiness, Prometheus metrics
L3navree-apiOpenAPI 3.1 from route metadata, docs UI, API resources
L3navree-mailMailables from templates; SMTP, log, array and failover transports; queued sending
L3navree-realtimePusher-protocol WebSocket server and a signed relay for other processes
L3navree-storageNamed disks on object_store, streaming, signed temporary URLs, upload checks
L4navree-sessionEncrypted session cookies, memory and database stores, CSRF
L4navree-validationValid<T> extractors on garde, class-validator style messages, async rules
L4navree-queueDatabase and memory job queues, workers, retries with backoff
L4navree-auditAudit trail of data changes, sign-ins, permission changes and denials
L4navree-accessRoles, permissions and CASL-compatible abilities, record checks, SQL list filters
L4navree-scheduleCron-style schedules, overlap prevention, single-node runs
L4navree-notifyNotifications over mail, database and broadcast, with read state
L5navree-authJWT access tokens, rotating refresh tokens, strategies, accounts, 2FA
L6navreeThe facade: re-exports every module crate and the macros
L6navree-cliThe navree binary: navree new, navree make:*, navree dev

The allowed edges

"Lower layers only" is the shape of the rule, but each crate has an explicit list of the Navree crates it may depend on. A layer-4 crate can't reach for any layer-3 crate it fancies; it gets the ones it needs, and adding one is a deliberate change to the list.

CrateMay depend on
navree-kernel, navree-macrosNo Navree crate
navree-contractskernel
navree-configkernel, contracts
navree-http, navree-log, navree-crypto, navree-events, navree-view, navree-cache, navree-http-clientkernel, contracts, config
navree-db, navree-testing, navree-observe, navree-apikernel, contracts, config, http
navree-mailkernel, contracts, config, view
navree-realtime, navree-storagekernel, contracts, config, http, crypto
navree-sessionkernel, contracts, config, http, crypto, db
navree-validation, navree-auditkernel, contracts, config, http, db
navree-queue, navree-schedulekernel, contracts, config, db
navree-accesskernel, contracts, config, http, db, cache
navree-notifykernel, contracts, config, http, db, mail
navree-authkernel, contracts, config, http, db, crypto, session, validation, cache, mail, access
navreeEvery runtime crate and the macros
navree-cliNo Navree crate: everything it knows about the framework is in the code it generates

The lists are what's allowed; some crates use less (navree-access doesn't use the cache today, and navree-auth doesn't use sessions). navree-config may use navree-contracts, which sits in the same layer; that's the one edge between crates of one layer.

Two exceptions apply to dev-dependencies, so tests can boot what they need:

  • A crate's tests may use any Navree crate in a lower layer. That's how every crate from L4 up tests with navree-testing (L3), and how navree-notify's tests boot ViewModule for MailModule.
  • navree-macros may dev-depend on any runtime crate, because its tests expand the macros against the crates the generated code calls.

The framework's own crates implement Module, Injectable and Controller by hand instead of using the macros. The macros sit at L0 with no runtime dependencies, and the generated code names whichever crates the user's crate depends on.

Cross-cutting needs: navree-contracts

Some needs cut across modules. The HTTP pipeline records refused requests in the audit trail; notifications broadcast to WebSocket channels; the database layer hands jobs to the queue after a commit. Letting navree-http depend on navree-audit would point up the hierarchy.

Instead, the shared shape lives in navree-contracts (L1) as a trait. One module implements it and registers it as a dyn Trait provider; the others resolve it, usually as optional:

ContractImplemented byUsed by
AuditSink, AuditEntry, AuditKindnavree-auditnavree-http (route audit actions, security events), navree-access (denials), navree-auth, navree-session (CSRF failures)
Broadcasternavree-realtimenavree-notify, navree-access, navree-auth (ending a user's subscriptions)
EventSinknavree-eventsModules that publish events by name
Dispatcher, Job, JobRegistration, JobOptionsnavree-queuenavree-events (queued listeners), navree-mail (queued mail), navree-notify, navree-schedule
HealthCheck, HealthStatusnavree-db, navree-cache, navree-storage (as multi-bindings)navree-observe (readiness)
SessionStore, SessionRecordnavree-session's memory and database storesThe session layer
Principal, AuthState, AuthVianavree-auth (through navree-http's Authenticator)Every module that needs the current user
ClockSystemClock, FixedClock (tests), registered by your appModules that need "now" and tests that fix it

Two plain types live there too: RequestId, which the HTTP pipeline creates for each request and which logs and audit entries carry, and Secret<T>, the wrapper configuration structs use for keys and passwords so they never show up in logs.

Your own code can use the contracts the same way. This function audits an export when the app has an audit trail, and does nothing otherwise:

Rust

And a service that must broadcast can inject the contract instead of the realtime module's types:

Rust

Two seams aren't in navree-contracts because they need HTTP types: Authenticator and RouteAuthorizer live in navree-http. navree-auth implements the first and navree-access the second, and the pipeline resolves both as dyn providers.

The dependency-graph test

crates/navree/tests/dependency_graph.rs encodes the table above, one row per workspace crate. Run it on its own:

Shell

It reads the workspace with cargo metadata and checks that:

  • every dependency edge between workspace crates (normal, dev and build) is allowed;
  • every workspace crate has a row, and every row is a workspace crate;
  • the table itself never points upwards and has no cycles;
  • Cargo.lock contains none of aws-lc-sys, aws-lc-rs, aws-lc-fips-sys, openssl, openssl-sys, native-tls or cmake: TLS is rustls with the ring provider everywhere, so building needs no C toolchain beyond what SQLite uses.

A forbidden edge fails with a message that says what the crate may use instead:

Text

The CRM is in the table too: crm-core at L7, crm-testkit at L8, the feature crates (crm-users, crm-companies, crm-deals, crm-activities) at L9 and the crm-api binary at L10. They may depend only on the navree facade and the CRM crates below them.

Two more tests in crates/navree/tests/facade.rs keep the facade's features in step with the hierarchy.

Applying the idea to your app

The same discipline works for an application. The CRM's feature crates all build on crm-core, which holds the shared entities, migrations, abilities and platform setup, and none of them depends on another feature crate. When two features need to cooperate, they do it through something lower down: an event, a trait in the shared crate, or a service the shared crate exports. Inside a single crate, modules give you the same structure: a feature imports only what it uses, and the boot fails when it reaches for something it didn't import.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks