Starter kits
Production-ready apps from one command: a SaaS kit with a Next.js and shadcn/ui front end, an API kit and a microservice kit.
On this page
Introduction
A starter kit is a complete application, written into your directory by navree new, that you own from the first commit. Instead of an empty app with one example module, you start with the parts every product needs already built, tested and wired together: sign-in, workspaces, billing, settings, an admin panel, background work, local services and a deploy configuration.
Pick the kit for the shape you are building:
| Kit | Command | What you get |
|---|---|---|
saas | navree new acme --kit saas | A Navree API and a Next.js and shadcn/ui front end: sign-in, two-factor, workspaces with members and roles, billing, settings and an admin panel, behind a backend-for-frontend |
api | navree new acme --kit api | The same API without the front end: accounts, workspaces, billing and OpenAPI with a committed contract, for mobile apps, partners or a front end you build yourself |
micro | navree new billing --kit micro | A microservice: a message handler and an event handler on NATS, health checks and metrics, and a contracts crate for its clients |
Kits are generated code, not a framework layer. There is no kit runtime and nothing to stay compatible with: every route, screen and test is a file in your repository, written with the same modules, macros and conventions as the rest of these docs.
Requirements
- Rust (the stable toolchain) and the Navree CLI.
- Docker or Podman, for the local services that Dock starts: TimescaleDB and Mailpit for
saasandapi, TimescaleDB and NATS formicro. - For the
saaskit, Node.js 22 or newer and pnpm.
Creating an app
navree new acme --kit saasCreated acme in /home/ada/code/acme (kit: saas)
Next steps:
cd acme
navree dock up # TimescaleDB and Mailpit
navree key:generate # then set APP_KEY in .env
navree migrate:fresh --seed # tables and a demo workspace
pnpm --dir web install # the web app's packages
navree dev # API on :8080, web app on :3000Start the local services
navree dock upreadsdock.tomland starts TimescaleDB (PostgreSQL with hypertables, Navree's default database) and Mailpit in containers.navree newalready wrote their settings into.env, between the# dock:startand# dock:endmarkers:APP_DATABASE__URLpoints at the database andAPP_MAIL__TRANSPORTat Mailpit's SMTP port. Mail the app sends shows up athttp://localhost:8025.Set the application key
navree key:generateprints a key; put it in.envasAPP_KEY. It encrypts cookies and secrets, and signs tokens, so keep it out of git (.gitignorealready lists.env).Create the tables and the demo data
navree migrate:fresh --seedruns every module's migrations and seeds one workspace,demo, with an owner ([email protected], also a super admin for/admin) and a member ([email protected]), both with the passwordpassword123, and three example projects.Run it
pnpm --dir web installinstalls the web app's packages.navree devserves the API, rebuilding it when the Rust code changes, and starts the web app's dev server (pnpm run devinweb/;--no-webskips it). Openhttp://localhost:3000and sign in as the owner.
navree new takes the same options with every kit: --navree-path <path> depends on a local Navree checkout, and --database postgres runs plain PostgreSQL instead of TimescaleDB (timescaledb = false in dock.toml; the kits run on PostgreSQL, so mysql and sqlite are for the minimal app). A kit brings its own front end or none: --kit saas --ui next is the same as --kit saas, and --ui next with api or micro is an error. The name may use lower-case letters, digits, - and _, and the target directory must be empty or missing. When rustfmt is installed, the generated Rust is formatted with it.
The SaaS kit
The SaaS kit is a working multi-tenant product: people sign up, create a workspace, invite their team, pick a plan and manage everything from settings, while you manage the whole system from an admin panel.
What users get
| Area | Screens and flows |
|---|---|
| Sign-in | Email and password, two-factor with an authenticator app and recovery codes, social sign-in buttons for every provider you configure |
| Accounts | Sign-up with email verification, forgotten password, password reset |
| Workspaces | Create a workspace (onboarding), switch between workspaces, workspace URLs under /w/<slug>/, accept an invitation |
| Members | Invite by email with a role, change a member's role (Admin, Member, Viewer), remove members, cancel invitations, transfer ownership |
| Billing | Plans with monthly and yearly prices, trials, checkout, the customer portal, invoices; seats follow the member count |
| Settings | Profile, password, two-factor, active sessions, workspace name and slug, notifications |
| Product | A dashboard and an example feature, projects, with a list, a status filter, search, pagination, a detail page, real-time updates and access rules per role |
| Admin | The Helm admin panel at /admin for super admins: users, workspaces and subscriptions |
How it is built
The API is a modular monolith. Its root adds the framework modules, configured once, then the kit's feature modules:
pub fn app(sources: ConfigSources) -> AppBuilder {
platform(Navree::app(), sources)
// Profile, sessions and sign-in options on top of AuthModule.
.module(modules::accounts::AccountsModule)
// Workspace settings, notification preferences and billing.
.module(modules::settings::SettingsModule)
// The example feature: a workspace-scoped resource.
.module(modules::projects::ProjectsModule)
// Helm resources for users, workspaces and subscriptions.
.module(modules::admin::AdminModule)
// `db:seed` / `migrate:fresh --seed`: the demo workspace.
.module(seed::SeedModule)
// navree:modules
}platform in src/platform.rs lists the framework modules the kit uses: Config, Logger, Crypto, Http, Database, Cache, Events, View, Mail, Queue, Schedule, Validation, Auth, Access, Workspace, Billing, Social, Notify, Realtime, Audit, Api, Observe and Helm, plus the authorizer of the private channel private-workspace.{id}, which only the workspace's members may subscribe to.
Every product route is scoped to a workspace. The example feature shows the pattern the rest of your features follow:
#[controller("/projects", version = "1")]
#[guard(InWorkspace::guard())]
impl ProjectsController {
/// `GET /api/v1/projects`: the current workspace's projects, newest
/// first; `?status=active&search=web&page=2&per_page=20`.
#[get("/")]
#[can(Read, Project)]
#[api(summary = "List projects", tag = "Projects", response = ProjectPage, responses(400, 401, 403))]
async fn index(
&self,
uow: WorkspaceUow,
ws: CurrentWorkspace,
scope: Accessible<Project, Read>,
Valid(Query(filter)): Valid<Query<ProjectFilter>>,
page: PageQuery,
) -> Result<Page<ProjectResource>> {
Ok(self.projects.list(&uow, ws.id(), scope, filter, page).await?)
}
/// `POST /api/v1/projects`
#[post("/")]
#[can(Create, Project)]
#[api(summary = "Create a project", tag = "Projects", request = CreateProject, response = ProjectResource, status = 201, responses(400, 401, 403))]
async fn store(
&self,
uow: WorkspaceUow,
ws: CurrentWorkspace,
CurrentUser(user): CurrentUser,
Valid(Json(input)): Valid<Json<CreateProject>>,
) -> Result<Created<ProjectResource>> {
let project = self.projects.create(&uow, ws.id(), user.id, input).await?;
Ok(Created(format!("/api/v1/projects/{}", project.id), project))
}
}The InWorkspace guard resolves the workspace from the request and checks that the user is a member before the access rules run, so #[can] and Accessible decide with the user's role in that workspace. The kit uses row isolation: the service reads and writes through WorkspaceRepo, so every query on a workspace table is filtered by workspace_id, and its migrations add PostgreSQL row-level security policies. The module registers m.workspace_scoped::<Project>(), so navree check verifies all of this. See Multi-tenant SaaS and Workspaces.
The front end is a Next.js app in web/ with shadcn/ui components, TanStack Query and react-hook-form. Its server is a backend-for-frontend: the browser calls only same-origin routes, and the BFF keeps the access token and the rotating refresh token in httpOnly cookies, refreshes them single-flight, checks the Origin of every write and forwards requests to the API with Authorization: Bearer. It also forwards the X-Workspace header the browser client sets from the /w/<slug>/ URL, and /admin, so Helm opens on the web app's origin with the same session.
The browser's API types are generated from the API's OpenAPI document: web/lib/api/schema.d.ts comes from the committed openapi.json. pnpm gen:api in web/ exports the document again and regenerates the types, so a changed DTO is a TypeScript error before it is a bug.
The generated layout
- AGENTS.mdconventions for coding agents
- CLAUDE.mdimports AGENTS.md
- Cargo.toml
- dock.tomllocal services: TimescaleDB, Mailpit
- harbor.tomlservers and deploys
- openapi.jsonthe API contract, checked in CI
- .envthis machine's settings (git-ignored)
- .env.example
- .github/
- workflows/
- ci.ymlfmt, clippy, check, migrations, tests, OpenAPI drift, web lint, tests, build and journeys
- workflows/
- config/
- app.toml
- src/
- main.rs
- lib.rs
- app.rsthe root: platform() plus the kit's modules
- platform.rsthe framework modules, configured once
- abilities.rsrole permissions and ability rules
- modules/
- accounts/
- settings/
- projects/the example feature, copy it for your own
- admin/
- seed.rsthe demo workspace and accounts
- tests/
- app.rsboots the app, checks the module graph and the seeder
- sign_up.rssign-up, verification and the first workspace
- accounts.rssessions and sign-in options
- projects.rsthe example feature, roles and isolation
- settings.rsworkspace settings and notifications
- billing.rscheckout and trials with the fake provider
- web/
- package.json
- pnpm-lock.yamlcommitted: CI and deploys install from it
- components.jsonshadcn/ui
- .env.exampleNAVREE_API_URL and the real-time settings
- .env.locala copy for this machine (git-ignored)
- app/
- (auth)/sign-in, sign-up, two-factor, verify, forgot and reset password
- (app)/
- w/[workspace]/dashboard, projects, settings
- api/
- [...path]/route.tsthe BFF: forwards with the bearer token
- auth/login, two-factor, logout, refresh, email verification
- admin/Helm, through the BFF
- auth/social sign-in redirect and callback
- broadcasting/
- auth/route.ts
- lib/
- bff/the backend-for-frontend, unit-tested
- api/the client, the generated schema and error helpers
- realtime.tslive updates on the workspace's private channel
- components/
- ui/shadcn/ui components
- e2e/Playwright journeys against the real API
Configuring it
Everything the kit needs is in config/app.toml and .env. The settings you will change first:
| Key | In the kit | What it does |
|---|---|---|
app.url | http://localhost:3000 | The web app's URL, for links in mail |
auth.registration | true | Public sign-up; set false to create accounts by invitation only |
workspace.resolvers | ["header"] | Where the workspace comes from: the browser client sends X-Workspace through the BFF |
workspace.roles.* | admin, member, viewer | What each workspace role may do (projects.read, billing.manage, ...) |
billing.provider | fake | fake for local development (checkout completes in the app); stripe or paddle in production, where harbor.toml sets APP_BILLING__PROVIDER=stripe |
billing.plans.* | starter, pro | Your plans, their trials and the provider's price ids |
social.providers.* | none | Social sign-in providers; each one adds a button |
mail.transport | Mailpit over SMTP, from .env | Your mail provider in production |
Secrets go in .env, never in the TOML file: APP_KEY, APP_BILLING__STRIPE__SECRET_KEY, APP_BILLING__STRIPE__WEBHOOK_SECRET and each social provider's client secret (APP_SOCIAL__PROVIDERS__GITHUB__CLIENT_SECRET).
The API kit
--kit api is the SaaS kit's API without web/: the same modules and the same tests, with its settings for API clients (app.url is the API's own address, and there is no CORS or front-end section).
- Accounts over the API. Sign-up, verification, password reset and two-factor are JSON routes under
/api/auth, with bearer tokens for every client. The OpenAPI document at/api/openapi.jsondescribes all of them, and/api/docsrenders it. - A committed contract. The kit ships
openapi.json;navree openapi:export openapi.jsonwrites it again, and the CI workflow fails when the committed file no longer matches the code, so every contract change shows in review. Generate typed clients from it. See API-first.
Use it for mobile apps, public APIs, or a front end you build in another stack. An API kit with a front end added later is the same app as the SaaS kit's API: the BFF pattern works with any web server.
The microservice kit
--kit micro creates one service that talks to others over a message broker, with navree-micro. It is a Cargo workspace of two crates, whose default member is the service, so cargo run, navree dev, navree check and every forwarded command run it from the root:
- AGENTS.md
- CLAUDE.md
- Cargo.tomlthe workspace; default-members = the service
- dock.tomlTimescaleDB and NATS
- harbor.toml
- .env.example
- .github/
- workflows/
- ci.ymlfmt, clippy, check, migrations, patterns, tests
- workflows/
- config/
- app.toml[micro] transport = "nats"
- crates/
- billing-contracts/the messages and events, shared with clients
- src/lib.rs
- billing/
- src/
- main.rs
- app.rsHttp and Observe (health and metrics), Auth, MicroModule, the feature modules
- modules/
- invoices/
- handlers.rs#[message_pattern] and #[event_pattern]
- service.rs
- entity.rs
- migrations/
- invoices/
- tests/
- app.rsboots the service, health checks, patterns
- invoices.rshandlers called through MicroFake, no broker needed
- src/
- billing-contracts/the messages and events, shared with clients
The handlers are ordinary injectable providers in a module, so they share the container, the access rules and the unit of work with the rest of your code:
#[injectable]
pub struct InvoiceHandlers {
invoices: Arc<InvoiceService>,
}
#[message_handlers]
impl InvoiceHandlers {
/// Request-response: another service asks for an invoice.
#[message_pattern("invoices.get")]
#[authenticated]
async fn get(
&self,
uow: Uow,
Valid(Payload(input)): Valid<Payload<GetInvoice>>,
) -> Result<Invoice> {
self.invoices.find(&uow, input.id).await
}
/// An event: a deal was won somewhere else.
#[event_pattern("deal.won")]
async fn deal_won(
&self,
uow: Uow,
Valid(Payload(event)): Valid<Payload<DealWon>>,
) -> Result<()> {
self.invoices.draft_for_deal(&uow, &event).await?;
Ok(())
}
}The module registers them with m.message_handlers::<InvoiceHandlers>(). draft_for_deal drafts one invoice per deal, so a repeated event changes nothing, and emits invoice.drafted with ServiceClient::emit, which the outbox publishes once the unit of work commits. An invalid event goes straight to the dead letters (navree micro:failed).
billing-contracts holds GetInvoice, Invoice, the invoices.get pattern (GET_INVOICE) and the events DealWon and InvoiceDrafted, each an Event whose NAME is its pattern. It depends on navree with the events feature only, so other services depend on it to call this one with ServiceClient and get the same types back:
let invoice: Invoice = client.send(GET_INVOICE, &GetInvoice { id }).await?;The caller's bearer token travels with the message, and the service's AuthModule verifies it, so give every service the same auth.jwt.secret (APP_AUTH__JWT__SECRET). The service creates no accounts of its own (auth.registration = false).
The service also serves /health/live, /health/ready (with a micro check that fails while the broker is unreachable) and /metrics over HTTP, so an orchestrator can probe it, and serve starts the HTTP server and the message listener together (micro:listen runs the listener alone). The kit sets micro.transport = "nats" and turns on the micro-nats feature in crates/billing/Cargo.toml; switch to Kafka, RabbitMQ, Redis Streams, MQTT or TCP by changing both, without touching the handlers. See Microservices.
tests/support/mod.rs boots the service with the committed config/app.toml on a fresh SQLite database and replaces the broker with MicroFake's in-memory transport (override_value with MicroOptions built from micro.transport()), so micro.client().send(..) and .emit(..) go through the same pipeline as over NATS: authentication, validation, the unit of work and the outbox.
Every kit includes
AGENTS.mdandCLAUDE.md, so a coding agent knows the project's layout, commands and conventions from its first prompt. They point at the agent skills for Navree's common tasks (navree skills:installcopies them into the project; ornpx skills add NavreeHQ/navree).dock.tomlfor Dock:navree dock upstarts the services the kit needs, and.envalready holds their settings. The CI workflow runs the same database image as a service container.harbor.tomlfor Harbor:navree harbor:provisionprepares a server andnavree deployships a release with a health check on/health/ready. In the SaaS kit, its[frontend]section deploysweb/in the same release as the API, so a page and the API it calls always come from the same build.- A CI workflow in
.github/workflows/ci.yml: formatting, clippy,check(the module graph), the migrations on PostgreSQL and the Rust tests; forsaasandapi, the OpenAPI drift check; formicro,micro:patterns; forsaas, the web app's lint, type check, unit tests and build, and the Playwright journeys against the real API. - Tests for what it ships. Rust tests boot the app with
TestAppon a fresh database and use the fakes for mail, the queue, billing and the message broker. The SaaS kit's BFF and API client have unit tests (Vitest) against a scripted API, and its Playwright journeys cover sign-up, sign-in with two-factor, inviting a member, upgrading a plan through the fake checkout, and what members and viewers may do.
Customizing a kit
The code is yours from the first commit, so change it the way you change any Navree app:
- Add features with
navree make:module, then copy the shape ofprojects/: a workspace-scoped entity, theInWorkspaceguard andCurrentWorkspacein the controller, an access subject and a page inweb/app/(app)/w/[workspace]/. In themicrokit, run the generators incrates/<service>. - Remove what you don't need. Delete the module and its line in
src/app.rs; for billing, also removeBillingModulefromplatform.rs, the billing screens underweb/app/(app)/w/[workspace]/settings/billing/and the billing tests.navree checktells you if anything still depends on it. - Restyle the front end. The colors, radius and fonts are CSS variables in
web/app/globals.css, and the components inweb/components/ui/are shadcn/ui source files you edit directly. Add more withpnpm dlx shadcn@latest add <component>inweb/: the app runs the shadcn CLI without depending on it, and the one stylesheet it used from theshadcnpackage is vendored asweb/app/shadcn-tailwind.css. - Change roles and permissions in
config/app.toml([workspace.roles.*]) andsrc/abilities.rs. The front end reads the rules fromGET /api/me/abilitiesandGET /api/v1/settings/abilities, so buttons follow the API's rules without front-end changes.
Updating
There are two kinds of updates, and neither rewrites your code.
The framework comes from the navree crate. Raise its version in Cargo.toml (or run cargo update -p navree), then follow the release notes. The web app's dependencies are ordinary npm packages.
The kit is a template, so improvements to it reach new apps, not yours. Your app records the kit it started from in Cargo.toml ([workspace.metadata.navree] in the micro kit's workspace):
[package.metadata.navree]
kit = "saas"
kit_version = "0.1.0"To adopt a later kit's changes, generate a fresh copy with the current CLI and compare the parts you care about:
navree new acme-latest --kit saas
git diff --no-index acme-latest/web/lib/bff acme/web/lib/bffThe release notes list what changed in each kit, so you can pick up a fix without reviewing the whole template.