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:
| Service | Technology | Port | Docker Image |
|---|---|---|---|
| API | Hono + Node.js | 3000 | toast-api |
| Admin | React 19 + Vite + nginx | 5173 | toast-admin |
| Docs | Next.js + Fumadocs | 3300 | toast-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 → PostgreSQLGlobal middleware stack
Applied to every request in this order (defined in apps/api/src/app.ts):
| Order | Middleware | Path | Purpose |
|---|---|---|---|
| 1 | onError | * | Structured error responses (env-aware detail levels) |
| 2 | requestId | * | Assigns unique ID for log correlation |
| 3 | requestLogger | * | Logs method, path, status, duration |
| 4 | cors | * | CORS with admin origin allowlist |
| 5 | session | /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
| Path | Module | Auth Required |
|---|---|---|
/healthz | healthRoutes | No |
/api/audit | auditRoutes | Yes |
/api/auth | authRoutes | Varies |
/api/capabilities | capabilitiesRoutes | No |
/api/collaboration | collaborationRoutes | Yes |
/api/content | contentRoutes | Yes |
/api/public/content | publicApiRoutes | No |
/api/settings | settingsRoutes | Yes |
/api/site | siteRoutes | No |
/api/users | usersRoutes | Yes |
/api/uploads | uploadRoutes | Yes |
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 driverDrivers 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.