This file provides a comprehensive overview of the OpenStatus project, its architecture, and development conventions to be used as instructional context for future interactions.
OpenStatus is an open-source synthetic monitoring platform. It allows users to monitor their websites and APIs from multiple locations and receive notifications when they are down or slow.
The project is a monorepo managed with pnpm workspaces and Turborepo. It consists of several applications and packages that work together to provide a complete monitoring solution.
- Frontend:
- Next.js (with Turbopack)
- React
- Tailwind CSS
- shadcn/ui
- tRPC
- Backend:
- Hono (Node.js framework)
- Go
- Database:
- Turso (libSQL)
- Drizzle ORM
- Data Analytics:
- Tinybird
- Authentication:
- NextAuth.js
- Build System:
- Turborepo
The OpenStatus platform is composed of three main applications:
apps/dashboard: A Next.js application that provides the main user interface for managing monitors, viewing status pages, and configuring notifications.apps/server: A Hono-based backend server that provides the API for the dashboard application.apps/checker: A Go application responsible for performing the actual monitoring checks from different locations.
These applications are supported by a collection of shared packages in the packages/ directory, which provide common functionality such as database access, UI components, and utility functions.
The project can be run using Docker (recommended) or a manual setup.
- Copy the example environment file:
cp .env.docker.example .env.docker
- Start all services:
docker compose up -d
- Access the applications:
- Dashboard:
http://localhost:3002 - Status Pages:
http://localhost:3003
- Dashboard:
- Install dependencies:
pnpm install
- Initialize the development environment:
pnpm dx
- Run a specific application:
pnpm dev:dashboard pnpm dev:status-page pnpm dev:web
To run the test suite, use the following command:
Before running the test you should launch turso dev in a separate terminal:
turso devThen, seed the database with test data:
cd packages/db
pnpm migrate
pnpm seedThen run the tests with:
pnpm test- Monorepo: The project is organized as a monorepo using pnpm workspaces. All applications and packages are located in the
apps/andpackages/directories, respectively. - Build System: Turborepo is used to manage the build process. The
turbo.jsonfile defines the build pipeline and dependencies between tasks. - Linting and Formatting: The project uses Biome for linting and formatting. The configuration can be found in the
biome.jsoncfile. - Code Generation: The project uses
drizzle-kitfor database schema migrations. - API: The backend API is built using Hono and tRPC. The API is documented using OpenAPI.
All workspace-scoped business logic lives in packages/services — not in tRPC routers. Routers stay thin: validate input, call a service verb, map errors. This keeps logic reusable across tRPC, Hono, and background jobs, and keeps it Edge-safe (the dashboard runs tRPC on Next.js Edge, so service code must avoid node:* imports).
Conventions for any new mutation:
- One file per verb under
packages/services/src/<entity>/(e.g.create.ts,update.ts,remove.ts), re-exported from the entity'sindex.ts. Routers import from@openstatus/services/<entity>. - Standard signature:
async function verbEntity(args: { ctx: ServiceContext; input: VerbInput }): Promise<...>.ctxcarriesworkspace,actor, and an optionaldb/transaction. Parse input with the schema at the top of the function. - Wrap mutations in
withTransaction(ctx, async (tx) => { ... })— it reuses an outer tx if present, otherwise opens one. Always passtx(notdefaultDb) to writes inside the block. - Workspace scoping is mandatory. Every read/write filters by
ctx.workspace.id. Use thegetXInWorkspacehelpers ininternal.tsfor fetch-or-throw. - Throw
ServiceErrorsubclasses (NotFoundError,ForbiddenError, etc. from./errors). Routers convert them viatoTRPCError. - Emit an audit row for every mutation via
emitAudit(tx, ctx, entry)inside the same transaction. Fail-closed: a failed audit insert rolls back the mutation. Seepackages/services/src/audit/emit.ts.- For updates, pass both
before(pre-mutation snapshot) andafter(post-.returning()row).changed_fieldsis auto-diffed; no-op updates are skipped. - For creates/deletes, pass only
afteror onlybefore. - Strip secrets from snapshots before emitting (e.g.
credential, bot tokens, raw API keys) — seeintegration/remove.tsfor the pattern. - Action names follow
{entity}.{verb}(monitor.update,integration.delete). Add new variants to the discriminated union in@openstatus/db/src/schema/audit_logs/validation.ts.
- For updates, pass both
- Tests live in
packages/services/src/<entity>/__tests__/and useexpectAuditRow({ workspaceId, action, entityId, ... })frompackages/services/test/helpers.tsto assert the audit side-effect. Each suite scopes to its own workspace and clearsaudit_logbetween cases.
When adding a router endpoint, the default answer is "write the service verb first, then call it from the router." Inline DB access in routers is a smell — it bypasses the audit log and the Edge-safety guarantee.