Skip to content

Structure & stack

Directory layout

app/
  (public)/            Public status page, history, service detail, badge,
                       legal pages, subscribe/verify, unsubscribe, my data
  admin/
    login/             Login page (no guard)
    (dashboard)/       Protected admin pages with the navigation layout
      sla-reports/     SLA reports; route handlers [id]/pdf, [id]/xlsx and adhoc
  api/
    status/            Public status API (JSON)
    health/            Infrastructure health check
    outage/            Status change routes for API tokens
    incidents/         Create incidents via API token
    maintenances/      Create maintenance windows via API token
  api-docs/            Swagger UI for the OpenAPI document
  badge.svg/           Dynamic SVG status badge
  feed.xml/            RSS feed of incidents
components/            Status page UI and admin widgets
components/ui/         shadcn/ui components (Base UI)
instrumentation.ts     Startup check when the server starts (SESSION_SECRET)
lib/
  actions/             Server Actions (return ActionResult<T>)
  api/                 JSON response helpers for API routes
  audit/               Audit log writer and wrapper
  auth/                Passwords, sessions, role guards, API token checks, rate limiting
  crypto/              AES-256-GCM helpers for secrets
  db/
    schema.ts          Drizzle schema and enum constants (source of domain types)
    queries/           Database access
    seed.ts            Demo data
  domain/              Business logic (status, uptime, SLA, timezones, roles)
  email/               E-mail templates, transports, processor
  env/                 Environment checks at startup
  export/              SLA export: context loading, Excel workbook, response, texts en/de
  notifications/       Dispatch, queues, schedulers, uptime checker
  pdf/                 SLA report PDF generation
  push/ sms/ webhooks/ Channel-specific transports, payload builders, processors
  validations/         Zod schemas
  viewer-timezone.ts   Visitor time zone toggle (logic and store)
i18n/                  next-intl request configuration
messages/              en.json and de.json
scripts/               Worker, migrations, backup/restore, seeding, helper scripts
drizzle/               SQL migrations and drizzle-kit metadata
tests/                 Unit tests, integration tests, factories
e2e/                   Playwright specs
public/                Static files, openapi.json, service worker for push

Layering rules

The code follows a strict direction of dependencies:

  1. Pages and routes (Server Components, route handlers) read data and render. They call queries or domain functions.
  2. Server Actions (lib/actions/) handle forms. They check the role, validate input with Zod, run the query inside a transaction, and call revalidatePath.
  3. Queries (lib/db/queries/) bundle the database access.
  4. Domain (lib/domain/) contains mostly pure functions without I/O, for example uptime calculation, SLA windows, severity ordering and timezone conversion. They are unit-testable without a database.

A few places access the database directly on purpose: applyComponentStatusChange (lib/domain/component-status.ts) opens the transaction for status changes, the e-mail processor (lib/email/processor.ts) writes the delivery status, and /api/health runs a SELECT 1.

A typical form submission looks like this:

Form → Server Action → role gate (getCurrentUser + ROLES_CAN_*)
     → Zod validation → query in transaction → revalidatePath

Public page data flow

Page (Server Component) → getStatusPageData() → queries → PostgreSQL

getStatusPageData() bundles all queries needed for the public page, so the page stays a thin rendering layer.

Conventions

  • Input from forms and APIs is untrusted and always parsed with Zod.
  • Logging uses the built-in logger in lib/log.ts: structured JSON in production, pretty output in development. console.* is only used in error boundaries and CLI scripts.
  • Audit entries are written with logAudit, which never throws. A failed audit write must not fail the business action.
  • Time zones: values are stored as timestamptz. Display uses settings.timezone and settings.display_timezone, and wall-clock input is converted with DST awareness (lib/domain/timezone.ts). On the public portal, visitors can switch to their browser zone. The choice is stored in the browser (localStorage tz-mode); the server always renders in the app zone.
  • Date formatting without an explicit locale and time zone (bare toLocale*) is forbidden, because it causes hydration errors. A unit test checks this for app/, components/ and lib/.