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