# 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.

## 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](https://navree.com/docs/getting-started/installation.md).
- Docker or Podman, for the local services that [Dock](https://navree.com/docs/tools/dock.md) starts: [TimescaleDB](https://navree.com/docs/database/timescaledb.md) and Mailpit for `saas` and `api`, TimescaleDB and NATS for `micro`.
- For the `saas` kit, Node.js 22 or newer and pnpm.

## Creating an app

```sh
navree new acme --kit saas
```

```text
Created 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 :3000
```

### 1. Start the local services

`navree dock up` reads `dock.toml` and starts TimescaleDB (PostgreSQL with hypertables, Navree's default database) and Mailpit in containers. `navree new` already wrote their settings into `.env`, between the `# dock:start` and `# dock:end` markers: `APP_DATABASE__URL` points at the database and `APP_MAIL__TRANSPORT` at Mailpit's SMTP port. Mail the app sends shows up at `http://localhost:8025`.

### 2. Set the application key

`navree key:generate` prints a key; put it in `.env` as `APP_KEY`. It encrypts cookies and secrets, and signs tokens, so keep it out of git (`.gitignore` already lists `.env`).

### 3. Create the tables and the demo data

`navree migrate:fresh --seed` runs every module's migrations and seeds one workspace, `demo`, with an owner (`owner@acme.test`, also a super admin for `/admin`) and a member (`member@acme.test`), both with the password `password123`, and three example projects.

### 4. Run it

`pnpm --dir web install` installs the web app's packages. `navree dev` serves the API, rebuilding it when the Rust code changes, and starts the web app's dev server (`pnpm run dev` in `web/`; `--no-web` skips it). Open `http://localhost:3000` and 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](https://navree.com/docs/tools/helm.md) admin panel at `/admin` for super admins: users, workspaces and subscriptions |

### How it is built

The API is a [modular monolith](https://navree.com/docs/patterns/modular-monolith.md). Its root adds the framework modules, configured once, then the kit's feature modules:

```rust title="src/app.rs"
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:

```rust title="src/modules/projects/controller.rs"
#[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](https://navree.com/docs/patterns/multi-tenancy.md) and [Workspaces](https://navree.com/docs/workspaces/introduction.md).

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](https://navree.com/docs/patterns/backend-for-frontend.md): 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

```text title="acme/"
AGENTS.md                 # conventions for coding agents
CLAUDE.md                 # imports AGENTS.md
Cargo.toml
dock.toml                 # local services: TimescaleDB, Mailpit
harbor.toml               # servers and deploys
openapi.json              # the API contract, checked in CI
.env                      # this machine's settings (git-ignored)
.env.example
.github/
  workflows/
    ci.yml                # fmt, clippy, check, migrations, tests, OpenAPI drift, web lint, tests, build and journeys
config/
  app.toml
src/
  main.rs
  lib.rs
  app.rs                  # the root: platform() plus the kit's modules
  platform.rs             # the framework modules, configured once
  abilities.rs            # role permissions and ability rules
  modules/
    accounts/
    settings/
    projects/             # the example feature, copy it for your own
    admin/
  seed.rs                 # the demo workspace and accounts
tests/
  app.rs                  # boots the app, checks the module graph and the seeder
  sign_up.rs              # sign-up, verification and the first workspace
  accounts.rs             # sessions and sign-in options
  projects.rs             # the example feature, roles and isolation
  settings.rs             # workspace settings and notifications
  billing.rs              # checkout and trials with the fake provider
web/
  package.json
  pnpm-lock.yaml          # committed: CI and deploys install from it
  components.json         # shadcn/ui
  .env.example            # NAVREE_API_URL and the real-time settings
  .env.local              # a 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.ts  # the 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.ts           # live 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.json` describes all of them, and `/api/docs` renders it.
- **A committed contract.** The kit ships `openapi.json`; `navree openapi:export openapi.json` writes 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](https://navree.com/docs/patterns/api-first.md).

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](https://navree.com/docs/microservices/introduction.md). 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:

```text title="billing/"
AGENTS.md
CLAUDE.md
Cargo.toml                # the workspace; default-members = the service
dock.toml                 # TimescaleDB and NATS
harbor.toml
.env.example
.github/
  workflows/
    ci.yml                # fmt, clippy, check, migrations, patterns, tests
config/
  app.toml                # [micro] transport = "nats"
crates/
  billing-contracts/      # the messages and events, shared with clients
    src/lib.rs
  billing/
    src/
      main.rs
      app.rs              # Http and Observe (health and metrics), Auth, MicroModule, the feature modules
      modules/
        invoices/
          handlers.rs     # #[message_pattern] and #[event_pattern]
          service.rs
          entity.rs
          migrations/
    tests/
      app.rs              # boots the service, health checks, patterns
      invoices.rs         # handlers called through MicroFake, no broker needed
```

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:

```rust title="crates/billing/src/modules/invoices/handlers.rs"
#[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:

```rust
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](https://navree.com/docs/patterns/microservices.md).

`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.md` and `CLAUDE.md`**, so a coding agent knows the project's layout, commands and conventions from its first prompt. They point at the [agent skills](https://navree.com/docs/ai/agent-skills.md) for Navree's common tasks (`navree skills:install` copies them into the project; or `npx skills add NavreeHQ/navree`).
- **`dock.toml`** for [Dock](https://navree.com/docs/tools/dock.md): `navree dock up` starts the services the kit needs, and `.env` already holds their settings. The CI workflow runs the same database image as a service container.
- **`harbor.toml`** for [Harbor](https://navree.com/docs/tools/harbor.md): `navree harbor:provision` prepares a server and `navree deploy` ships a release with a health check on `/health/ready`. In the SaaS kit, its `[frontend]` section deploys `web/` 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; for `saas` and `api`, the OpenAPI drift check; for `micro`, `micro:patterns`; for `saas`, 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 `TestApp` on 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 of `projects/`: a workspace-scoped entity, the `InWorkspace` guard and `CurrentWorkspace` in the controller, an access subject and a page in `web/app/(app)/w/[workspace]/`. In the `micro` kit, run the generators in `crates/<service>`.
- **Remove what you don't need.** Delete the module and its line in `src/app.rs`; for billing, also remove `BillingModule` from `platform.rs`, the billing screens under `web/app/(app)/w/[workspace]/settings/billing/` and the billing tests. `navree check` tells 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 in `web/components/ui/` are shadcn/ui source files you edit directly. Add more with `pnpm dlx shadcn@latest add <component>` in `web/`: the app runs the shadcn CLI without depending on it, and the one stylesheet it used from the `shadcn` package is vendored as `web/app/shadcn-tailwind.css`.
- **Change roles and permissions** in `config/app.toml` (`[workspace.roles.*]`) and `src/abilities.rs`. The front end reads the rules from `GET /api/me/abilities` and `GET /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](https://navree.com/docs/prologue/release-notes.md). 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):

```toml title="Cargo.toml"
[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:

```sh
navree new acme-latest --kit saas
git diff --no-index acme-latest/web/lib/bff acme/web/lib/bff
```

The release notes list what changed in each kit, so you can pick up a fix without reviewing the whole template.

- [Multi-tenant SaaS](https://navree.com/docs/patterns/multi-tenancy.md): How workspaces scope every request and query.
- [Backend for frontend](https://navree.com/docs/patterns/backend-for-frontend.md): The BFF the SaaS kit's front end is built on.
- [Billing](https://navree.com/docs/packages/billing.md): Plans, trials, seats and the customer portal.
- [Your first app](https://navree.com/docs/getting-started/your-first-app.md): Build an app from `navree new` step by step.
