Toast
Contributor

Architecture Overview

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 organized, what each package does, and how the API server processes requests.

Monorepo Layout

Toast is a pnpm workspace monorepo with four package groups:

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

shared/
  contracts/      # Shared TypeScript types, Zod schemas, permissions

packages/
  db/             # Drizzle ORM schema, migrations, seed
  ui/             # Shared React components (Base UI)
  config/         # Shared ESLint and TypeScript configuration

drivers/
  email-*         # Email provider drivers (Mailgun, Postmark, etc.)
  storage-*       # Storage provider drivers (S3, local filesystem)
  queue-*         # Queue provider drivers

Workspace Configuration

Defined in pnpm-workspace.yaml:

packages:
  - 'apps/*'
  - 'packages/*'
  - 'shared/*'
  - 'drivers/*'

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 Self-Hosting for deployment 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 (validation, response shaping)
    → Service (business logic, 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

Routes are mounted at their base paths in app.ts:

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

Layer Responsibilities

Routes — OpenAPI schema definitions, middleware composition, and handler delegation. Each route file uses @hono/zod-openapi to define request/response schemas.

Services — Business logic, validation rules, and event emission. Services never access the database directly — they call repositories.

Repositories — Database queries via Drizzle ORM. Every query is scoped by siteId for multi-tenancy. See Database Patterns for conventions.

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 — see ADR-012: Unified User Model for the design rationale.

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

// In any route handler, siteId is available via context
const siteId = c.get('siteId');

Authentication

Toast uses Better Auth for authentication. The auth configuration lives in apps/api/src/lib/auth.ts and supports:

  • 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 domain events for side effects like audit logging. The event system uses a simple pub/sub pattern:

  1. Services emit events (e.g., content.created, settings.updated)
  2. Subscribers handle side effects (e.g., writing audit log entries)
  3. Subscribers are registered at startup in app.ts

See ADR-007: Event System for the design.

Driver System

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

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

Drivers are loaded at startup based on environment variables. See Driver Configuration and Writing a Custom Driver.

Shared Packages

shared/contracts

TypeScript types, Zod schemas, and permission definitions shared between the API and admin panel. This package ensures type safety across the client-server boundary.

Key exports:

  • Permission statements — defines what actions exist per resource
  • Role presets — admin, editor, author, member
  • Zod schemas — shared validation for API request/response types

packages/db

Drizzle ORM schema definitions, migration files, and the seed script. This package owns the database schema — no other package should define tables.

packages/ui

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

packages/config

Shared ESLint and TypeScript configuration. Keeps tooling consistent across all packages.

OpenAPI Documentation

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

  • /docs/hono-openapi — Hono routes spec (internal)
  • /docs/openapi — Merged spec (Hono + Better Auth routes)
  • /docs — Interactive Scalar UI for exploring and testing endpoints

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

On this page