Architecture
One framework,every architecture.
Navree builds every shape of backend from the same unit: the module. Arrange modules one way and you have a modular monolith; another way, and you have services, a multi-tenant SaaS or an event-driven system. Start as one binary and change shape later without rewriting a module.
01 Modular monolith
One binary, hard boundaries.
Every feature is a module that declares what it imports, provides and exports. The app ships as one binary with one database, calls between features are function calls, and the kernel refuses to boot when a module reaches for something it wasn't given.
- request
- imports
#[module(
imports = [CompaniesModule],
providers = [DealService],
controllers = [DealsController],
exports = [DealService],
)]
pub struct DealsModule;Choose it when
- You're starting. Every Navree app begins here.
- One team, or several teams on one release.
- A request touches three features and should commit once.
How Navree supports it
#[module]with imports, providers, controllers and exports- A type-keyed DI container, checked at boot and by
checkin CI navree make:modulewrites a module and registers it- A crate per module when teams want Cargo to hold the lines
02 Microservices
Split out a service. Keep the module.
A module that needs its own deploys, scaling or on-call moves into a binary of its own. Services answer messages and react to events over NATS, Kafka, RabbitMQ, Redis Streams, MQTT, TCP or gRPC, each owns its database, and a message handler is written like a route.
- request-response
- event
#[message_handlers]
impl DealsHandlers {
#[message_pattern("deals.get")]
#[can(Read, Deal, from = "id")]
async fn get(&self, uow: Uow, deal: Authorized<Deal, Read>) -> Result<DealView> {
self.deals.view(&uow, deal.into_inner()).await
}
}Choose it when
- One feature scales or ships unlike the rest.
- A feature's failures or data must be kept apart.
- A team owns a feature end to end, on-call included.
How Navree supports it
MicroModule, with the transport in configuration#[message_pattern]and#[event_pattern]handlers with guards,#[can]and a unit of workServiceClient::sendandemit, with an outbox for events- Request id, caller, workspace and trace context on every message
03 Multi-tenant SaaS
Every customer gets a workspace.
Workspaces, members, roles and invitations are part of the framework and on by default. Each request resolves its workspace, checks membership and scopes its unit of work, so every query sees one customer's data, with PostgreSQL row-level security underneath.
- request
[workspace]
resolvers = ["header", "path"]
# row | schema | database
isolation = "row"
[workspace.roles.member]
permissions = ["projects.*"]Choose it when
- Many organizations share one deployment.
- Roles differ per customer: an owner here, a viewer there.
- Some customers need their own schema or database.
How Navree supports it
WorkspaceModule, in the default features- The
CurrentWorkspaceextractor andWorkspaceRepo - Isolation by row, schema or database, set in configuration
- Workspace roles feed
#[permissions]and#[can]
04 Event-driven
React to changes without coupling modules.
Modules emit typed events and listeners react, in the request or on a worker. Jobs and outgoing events are written in the same transaction as the change and leave only after it commits, so nothing hears about a change that rolled back.
- in the request
- after the commit
impl DealsModule {
fn wire(&self, m: &mut ModuleBuilder) {
m.listen::<DealAssigned, NotifyDealOwner>()
.on::<DealWon, NotifyDealOwner>();
m.forward_event::<DealWon>(); // also publish deal.won to other services
}
}Choose it when
- Side effects must not slow or fail the request.
- Screens should update live.
- Other services must hear about every committed change.
How Navree supports it
- Typed events (
Event) andm.listen::<E, L>() - Queued listeners and jobs on
queue:work, with retries and backoff - A transactional outbox:
jobsandmicro_outboxrows commit with the change - Broadcasting over the built-in Pusher-protocol WebSocket server
05 API-first
The contract comes from the code.
Routes declare their version and access rule, inputs are validated DTOs, outputs are typed resources and every error has one JSON shape. Navree builds an OpenAPI 3.1 document from all of it at boot, so the document, the validation and the generated clients change together.
- generated from
#[controller("/deals", version = "2")]
impl DealsV2Controller {
#[get("/{id}")]
#[can(Read, Deal, from = "id")]
#[api(summary = "Show a deal", response = DealV2Resource, responses(401, 403, 404))]
async fn show(
&self,
uow: Uow,
deal: Authorized<Deal, Read>,
) -> Result<Json<DealV2Resource>> {
let deal = self.deals.show(&uow, deal.into_inner()).await?;
Ok(Json(deal.into()))
}
}Choose it when
- Mobile apps, partners, other teams or AI agents use your API.
- A breaking change should fail CI, not a client.
- The docs must never drift from the code.
How Navree supports it
#[controller("/deals", version = "1")]and#[api(...)]Valid<Json<T>>with garde rules: field errors before the handler runsApiModuleserves/api/openapi.jsonand/api/docsopenapi:exportfor typed clients in any language
06 Backend for frontend
Keep every token out of the browser.
Your web app talks only to its own server, on the same origin. That server keeps the access and refresh tokens in httpOnly cookies, rotates them and calls the Navree API with a bearer token. The API stays the plain bearer-token API every other client uses.
- request
- real-time event
import { bff } from "@/lib/bff/server";
/** Every /api/* call is forwarded with the bearer token from the httpOnly cookie. */
function handle(request: Request): Promise<Response> {
return bff.proxy(request, new URL(request.url).pathname);
}
export { handle as DELETE, handle as GET, handle as PATCH, handle as POST, handle as PUT };Choose it when
- You ship your own web app.
- One cross-site scripting bug must not leak every session.
- Mobile apps and partners call the same API with tokens.
How Navree supports it
- Refresh tokens that rotate on every use, with reuse detection
- A Next.js BFF in the
saasstarter kit andnavree new --ui next - Real time straight from the WebSocket server, channels authorized through the BFF
- A browser client generated from the OpenAPI document
Grow without rewriting
Start as one binary. Grow into the shape the product needs.
A module keeps its providers, rules, migrations and tests whether it runs in the monolith, inside a workspace or in a service of its own. What changes is the wiring around it.
Step 1
Start as a modular monolith
navree newwrites one binary, andmake:moduleadds a module per feature. Calls between features are function calls, and one transaction covers a request.Terminal navree new crm navree make:module companies --crud navree make:module deals --crudStep 2
Add workspaces
Add
WorkspaceModule, give tenant tables aworkspace_idand takeCurrentWorkspacewhere a handler needs it. Routes, services and access rules stay as they are.src/app.rs .module(AuthModule::from_config()) .module(WorkspaceModule::from_config()) .module(CompaniesModule) .module(DealsModule)Step 3
Move a module into a service
When deals need their own deploys,
DealsModulemoves into its own binary withMicroModule. Its service, rules and tests come along; callers send a message instead of calling a method.src/modules/reports/service.rs self.deals.show(&uow, id).await? self.client .send("deals.get", &GetDeal { id }) .await?
- What changes
- The wiring: which binary a module runs in, its transport, the isolation of its tables, how callers reach it.
- What stays
- Every line of module code: its services, routes and handlers, its access rules, its migrations and its tests.
Start
Start from the shape you need.
Each starter kit is a working app in one of these shapes, built from the same modules as everything on this page.
Multi-tenant SaaS
Workspaces, billing and an admin panel, with a Next.js BFF that keeps tokens in httpOnly cookies.
$ navree new acme --kit saasAPI-first monolith
Versioned routes, an OpenAPI 3.1 document and a module per feature, ready for any client.
$ navree new acme --kit apiMicroservice
One module with a message handler, an event handler and NATS configured for development.
$ navree new deals --kit micro
