SaaS Starter
Getting Started

Project Structure

Monorepo layout, DDD layering, and where each thing lives.

Top level

apps/
  client/                Next.js — landing, dashboard, auth, /docs
  server/                Express — API, auth, billing, jobs
packages/
  contracts/             Shared Zod schemas + inferred types
  shared/                Permissions catalog, Result, errors, branded types
  tsconfig/              tsconfig bases
  eslint-config/         flat configs that enforce DDD layering
tests/                   Playwright e2e

The repo is a Bun workspace. Every internal cross-package import goes through @app/contracts, @app/shared, etc. — never relative across apps.

apps/server

src/
  index.ts               Entry point. otel-init MUST be the first import.
  otel-init.ts           Boots OTel before anything else loads.
  bootstrap/
    env.ts               Zod schema for env. Source of truth.
    container.ts         Awilix DI registry.
    app.ts               Builds the Express app and mounts all routers under /api.
    worker.ts            BullMQ worker bootstrap.
  infrastructure/
    db/                  Prisma client + helpers.
    http/                api-route helper, error handler, rate-limit, OpenAPI registry, requirePermission.
    events/              In-memory event bus.
    jobs/                BullMQ queue + worker factories, Bull Board.
    cache/               Redis adapter (no-op when REDIS_URL unset).
    logger/              pino setup.
    observability/       Sentry, OTel exporters, captureError helper.
    i18n/                Server-side translation runtime.
  modules/<context>/     Bounded contexts (see below).
  shared/
    aggregate-root.ts    AggregateRoot base + DomainEvent type.
    usecase.ts           UseCase<Input, Output> contract.
    value-object.ts      Value-object base.

DDD layering inside a module

Every module under apps/server/src/modules/<context>/ follows the same layout:

domain/                  Entities, value objects, repository interfaces.
                         No I/O. No framework imports. No Prisma. No Express.
application/             Use cases, ports (output adapters), DTOs.
infrastructure/          Prisma repos, third-party adapters, mappers.
interfaces/http/         Express controllers, route registration, Zod schemas.

The dependency rule is inward only: interfaces/ depends on application/, which depends on domain/. Infrastructure implements the ports declared in application/ (or repository interfaces declared in domain/).

ESLint enforces this. domain/ cannot import express, @prisma/client, or anything from application/ / infrastructure/ / interfaces/. Try it — the import will be flagged before commit.

The bounded contexts shipped today: iam, tenancy, billing, notifications, storage, audit, feature-flags. See Bounded contexts.

apps/client

app/
  page.tsx, es/, pt/     Landing pages (i18n).
  _landing/              Landing components + dictionary.
  (auth)/                Sign-in, sign-up, forgot-password.
  (dashboard)/           Authenticated app surface.
  docs/[[...slug]]/      Fumadocs.
  api/search/            Orama search endpoint.
  globals.css            Tokens (OKLCH) + base + utilities.
  layout.tsx             Loads Geist Sans + Geist Mono once.
components/
  brand/                 UseDeploy design system primitives. Read /docs/frontend.
  ui/                    shadcn-derived primitives.
content/docs/            The MDX you're reading.
lib/
  api/                   openapi-fetch client + generated types.
  validations/           Client-only refinements built on @app/contracts.
  source.ts              Fumadocs content source.

The client speaks to the server via openapi-fetch and the typed paths in lib/api/openapi-types.ts. Never redefine a server-known field in a client Zod schema — extend the contract, don't fork it.

packages/contracts

Zod schemas + inferred types that cross the client/server boundary. Forms and mutations import from here:

src/
  auth.ts                Login, register, password reset.
  user.ts                User profile, update, list.
  common.ts              Shared primitives (pagination, IDs).
  index.ts               Barrel.

If a server endpoint changes its body or response, the contract changes here, the OpenAPI is regenerated (bun run generate:api), and the client compile-checks the new shape.

packages/shared

src/
  permissions/           resource:action catalog + hasPermission helper.
  result/                Result<T, E> + ok/err constructors.
  errors/                DomainError, UnauthorizedError, ForbiddenError, etc.
  types/                 Branded ID types.
  index.ts               Barrel.

Imported as @app/shared from both apps.

On this page