Toast
Contributor

Architecture Concepts

Monorepo layout, packages, API middleware stack, and how it all fits together.

This page describes the high-level architecture of the Toast codebase — how the monorepo is organised, what each package does, and how the API server processes requests.

Monorepo layout

Toast is a pnpm workspace monorepo with four package groups. The placement test: "Could this package be useful in a completely different application?" If yes → packages/. If it changes every time you add a feature → shared/.

apps/
  api/            # Hono API server (Node.js)
  admin/          # React 19 admin panel (Vite + nginx)
  docs/           # Documentation site (Next.js + Fumadocs)

shared/
  contracts/      # Domain types, Zod schemas, permissions
  db/             # Drizzle table definitions, migrations, seed data

packages/
  core/           # Infrastructure contracts (Database, Logger, EventBus)
  drizzle/        # Drizzle connection, multi-tenant helpers, pagination
  ui/             # Shared React components (Base UI)
  config/         # Shared TypeScript and Vitest configuration
  drivers/        # Driver interfaces and loaders (email, storage, queue)
  td/             # Toast Dev CLI — inspection and scaffolding tooling
  mcp/            # MCP server exposing td commands to AI coding assistants

drivers/
  email-*         # Email provider drivers (Mailgun, etc.)
  storage-*       # Storage provider drivers (S3, MinIO, R2)

Defined in pnpm-workspace.yaml. Turborepo (turbo.json) orchestrates builds, ensuring correct dependency order across packages.

Deployable services

Toast produces three independently deployable services:

ServiceTechnologyPortDocker Image
APIHono + Node.js3000toast-api
AdminReact 19 + Vite + nginx5173toast-admin
DocsNext.js + Fumadocs3300toast-docs

All three services are built as Docker images in CI and deployed to Railway. See Docker for build details.

API server architecture

The API server follows a strict layered architecture. Requests flow through layers in order, and no layer may skip one below it.

Request flow

Request
  → Global Middleware (requestId, logger, CORS)
  → Session Middleware (extracts user/session/siteId)
  → Route Handler
    → requireAuth / requirePermission middleware
    → Controller (extract auth context, call service, shape response)
    → Service (business logic, validation, events)
    → Repository (database queries via Drizzle)
    → Drizzle ORM → PostgreSQL

Global middleware stack

Applied to every request in this order (defined in apps/api/src/app.ts):

OrderMiddlewarePathPurpose
1onError*Structured error responses (env-aware detail levels)
2requestId*Assigns unique ID for log correlation
3requestLogger*Logs method, path, status, duration
4cors*CORS with admin origin allowlist
5session/api/*Extracts user, session, siteId from cookies

The session middleware is scoped to /api/* so health checks (/healthz) respond immediately without touching the database — critical for Railway deployment health probes.

Route groups

PathModuleAuth Required
/healthzhealthRoutesNo
/api/auditauditRoutesYes
/api/authauthRoutesVaries
/api/capabilitiescapabilitiesRoutesNo
/api/collaborationcollaborationRoutesYes
/api/contentcontentRoutesYes
/api/public/contentpublicApiRoutesNo
/api/settingssettingsRoutesYes
/api/sitesiteRoutesNo
/api/usersusersRoutesYes
/api/uploadsuploadRoutesYes

Dependency injection and the composition root

Toast uses factory functions with explicit dependency injection — no DI container, no decorators, no global singletons. Each layer is a factory function that receives its dependencies as parameters.

const infra = buildInfrastructure(config);
const stacks = buildStacks({
  db: infra.database.db,
  eventBus: infra.eventBus,
  logger: infra.logger,
  config,
});

const routes = {
  content: createContentRoutes({
    contentService: stacks.contentService,
    versionService: stacks.versionService,
  }),
};

const app = createApp({
  config,
  logger: infra.logger,
  auth: stacks.auth,
  routes,
});

apps/api/src/container.ts is the composition root for infrastructure and stacks. index.ts and app.ts still participate in the final runtime wiring so the HTTP surface remains explicit.

This pattern:

  • Makes the dependency graph explicit and visible in one place
  • Eliminates import-order bugs (auth can't be constructed without a database reference)
  • Makes service tests trivial — pass plain mock objects, no vi.mock() needed
  • Isolates third-party libraries behind interfaces (AuthProvider, EventBus, Database)

See ADR-014 for the full rationale.

Multi-tenancy

Every database table has a siteId foreign key with an index. Every repository method filters by siteId. This is a non-negotiable rule.

The siteId is extracted from the user's session by the session middleware and stored in Hono's context:

const { siteId } = getAuthContext(c);

See ADR-012 for the design rationale.

Authentication

Toast uses Better Auth for authentication, behind an AuthProvider interface. Better Auth's internals don't leak past apps/api/src/providers/auth.provider.ts. The rest of the application sees only ToastUser and ToastSession from explicit @toast/contracts/auth imports.

Supported auth methods:

  • Email/password sign-in and sign-up
  • Magic link authentication
  • Session management with cookie-based tokens

See Authentication for integration details and RBAC Internals for the permission model.

Event system

Services emit typed domain events via the EventBus interface from @toast/core. Subscribers handle side effects (audit logging, etc.) and are registered at startup.

// In a service factory
deps.eventBus.emit(createContentCreatedEvent({ contentId, siteId, actorId }));

See Events for the full event catalogue and subscriber pattern.

Driver system

External integrations (email, storage) are abstracted behind driver interfaces. Each driver is an independent package in drivers/:

drivers/
  email-mailgun/     # Mailgun email driver
  storage-s3/        # AWS S3 / MinIO storage driver

Drivers implement typed interfaces from packages/drivers and are loaded at startup from typed config objects. The config layer reads environment variables once, performs the startup validation/completeness checks, then passes typed config into the loader APIs and onward into the app's injected dependencies.

See Driver Overview, Email, and Storage.

Shared packages

shared/contracts

Domain-specific TypeScript types and Zod schemas shared between the API and admin panel:

  • ToastUser, ToastSession — canonical auth types (used everywhere)
  • Permission statements and role presets
  • API request/response schemas (content, users, site, capabilities)

shared/db

Drizzle table definitions, relations, migrations, and seed data. Lives in shared/ because it changes at the feature cadence — every new resource adds a table here. Imported by repositories only.

packages/core

Infrastructure contracts — AuthProvider, Database, EventBus, Logger. These are the DI seams that define what each module must satisfy. Zero application dependencies — could be extracted to its own repo.

packages/drizzle

Drizzle ORM wrapper: database client factory, withSiteId() multi-tenancy helper, paginate() utility, and re-exports of common Drizzle operators (eq, and, desc, etc.). Framework-level code with no knowledge of Toast's domain.

packages/ui

Shared React components built on Base UI. Single source of truth for UI primitives — apps must not create their own component libraries.

packages/td

The Toast Dev CLI. Inspection, scaffolding, and dev lifecycle tooling. See Toast Dev CLI.

OpenAPI documentation

The API auto-generates OpenAPI 3.1 specs from route definitions:

  • /docs — merged spec (Hono + Better Auth routes)

The merged spec is used to auto-generate the API Reference section of this documentation site.

On this page