Monorepo layout
BoringStack ships as a single monorepo. API, UI, docs site, and Compose infrastructure live in one tree with path-filtered CI and root-level bun run regen / bun run check for cross-app contracts. Click Use this template on the boringstack-xyz/boringstack GitHub repo to create your fork; all pieces come in one clone.
Top-level directories
Section titled “Top-level directories”boringstack/ apps/api/ Bun + Elysia HTTP API; Drizzle schema + migrations apps/ui/ Vite + React SPA; OpenAPI client generated from /swagger/json apps/docs/ Astro Starlight site (boringstack.xyz) apps/site/ Astro static marketing site on the apex domain (see Public site) packages/ Shared code used by more than one app (optional; see below) infra/compose/ Docker Compose runtime; services, env vars, profiles, overlays infra/k3s/ Optional Kustomize + ArgoCD target for a Kubernetes cluster infra/bootstrap/ Optional OpenTofu for VPS provisioning on Hetzner + CloudflareEach app keeps its own package.json, lockfile, and lint config. CI workflows (.github/workflows/) have path filters like apps/api/** and infra/compose/** so each app’s changes trigger only its own pipeline.
Why one repo?
Section titled “Why one repo?”Smaller context: Open apps/ui for UI work without loading the entire API schema graph.
Independent releases: Compose publishes latest and sha-* images per app for WUD, and pins versions via env vars. The Kubernetes target ships the API, migrations, UI and site as one digest-pinned set. See Production releases.
Explicit join points: OpenAPI schema, ACL types, lint-meta RULES, and docs catalogs are regenerated from the repo root with bun run regen, so drift is caught by CI.
Compose build contexts
Section titled “Compose build contexts”The infra/compose/compose/docker-compose.yml defines api-dev, api, ui-dev, and ui services with build contexts pointing back to ../../../apps/api and ../../../apps/ui. In prod, the default image: reference pulls pre-built images from GHCR published by release workflows; in dev, build: reads the local source and sets up a bind mount for hot reload.
Env files:
compose/.env.example: base stack and observability vars.compose/api.dev.env.example: API-specific dev overrides.compose/api.prod.env.example: API-specific prod secrets (JWT, MFA key, etc.).
Orchestration:
compose/dev.shreadsSTACK=andWITH_*=to assemble thedocker-composeinvocation with the right overlays and profiles.
What lives where
Section titled “What lives where”apps/api
Section titled “apps/api”HTTP routes, auth, OAuth, password hashing, email send and provider abstraction, BullMQ jobs and queue workers, audit log writes, Drizzle schema and migrations.
apps/ui
Section titled “apps/ui”Pages, components, queries, stores, routes. OpenAPI client generated
from /swagger/json. i18n (en, de), shadcn/ui components, Storybook,
Playwright e2e.
infra/compose
Section titled “infra/compose”docker-compose YAMLs and profile overlays. Prometheus, Grafana, Loki, Promtail configs. Traefik labels, ACME, security middlewares. Backup scripts and runbooks.
apps/docs
Section titled “apps/docs”boringstack.xyz Starlight site. Generated lint-meta and scripts tooling catalogs.
apps/site
Section titled “apps/site”The public marketing site at the apex domain: static pages and a Markdown blog, built by Astro and served by nginx. It has no server code and makes no API calls. See Public site.
infra/bootstrap (optional)
Section titled “infra/bootstrap (optional)”Hetzner provisioning, Cloudflare DNS, cloud-init.
Shared packages
Section titled “Shared packages”Code used by two or more apps goes in packages/<name>/. Code used by one app stays in
that app. A package holds no secrets and no app-specific routes or pages. The full
convention is in apps/ui/docs/agents/shared-packages.md.
Each package has package.json with "private": true, src/index.ts as its public
entry, a tsconfig.json that extends the UI config, and an eslint.config.mjs that
imports the UI rule set. Its lint, format:check and typecheck scripts run the
binaries in apps/ui/node_modules, so run bun install in apps/ui first. Its
validate script runs all three, and lint uses --max-warnings=0.
Packages are discovered, not listed:
bun run checkrunsvalidatein everypackages/*/that has apackage.json. A package without avalidatescript fails the check. A repo with nopackages/directory gets no package checks and no error.scripts/ci/pre-push.shruns the same gate when anything underpackages/changed, even if no app changed..github/workflows/apps-ui-validate.ymlruns it onpackages/**changes.- Dependabot has no entry for
packages/, because it fails on a directory that does not exist. Add abunentry for the package’s directory when you create it.
Wiring a package into apps/ui (tsconfig paths, server.fs.allow, resolve.dedupe) is
a separate step, described in the same guide. Note that lint:meta does not scan
packages/, so the package’s own ESLint config is its only architecture gate.
Cross-app contracts
Section titled “Cross-app contracts”From the repo root:
bun run regen # ACL types → OpenAPI schema → RULES.md → docs JSONbun run check # drift checks (api on :7330 required for OpenAPI check)| Contract | Producer | Consumer | Generated artifact |
|---|---|---|---|
| ACL types | apps/api | apps/ui | apps/ui/src/lib/acl/acl.types.generated.ts |
| OpenAPI | apps/api /swagger/json | apps/ui | apps/ui/src/lib/api/schema.d.ts |
| lint-meta | each app scripts/lint-meta/ | committed RULES.md | per app |
| Docs catalogs | api + ui scripts/README | apps/docs | src/data/*.json |
CI runs the same checks from one checkout on GitHub main.
The contract between API and UI
Section titled “The contract between API and UI”The API exposes its OpenAPI document at /swagger/json. The UI ships bun run generate:api that reads that document and rewrites src/lib/api/schema.d.ts. When the API changes routes, the UI either fails typecheck or you run bun run regen at the repo root.
See OpenAPI client for the full flow.
The contract between infra and the apps
Section titled “The contract between infra and the apps”infra/compose/compose/docker-compose.yml defines api-dev / api / ui-dev / ui services whose build context is apps/api and apps/ui. Env vars are documented in compose/api.dev.env.example and compose/api.prod.env.example.
In prod the default image: reference pulls a pre-built image from GHCR, published by the release workflows. See Deployment.