BoringStack is a monorepo. Each app has its own contributor guide with the patterns and lint rules specific to that surface:
- apps/api/CONTRIBUTING.md — Elysia routes,
Drizzle schemas, audit log requirements, the
bun run checkcontract. - apps/ui/CONTRIBUTING.md — feature folders, TanStack Query patterns, i18n, RTL tests.
- apps/docs/DEPLOY.md — how the boringstack.xyz docs site is wired up on Cloudflare Pages (build settings, custom domain, rollback flow).
This document covers what's true for any change anywhere in the repo.
From a fresh fork:
./setup.sh --upThat copies .env, generates the GlitchTip secret, and brings the full
Compose stack up: Postgres, Valkey, api-dev with migrations applied,
ui-dev on localhost:7331, OpenAPI client generated. No local Bun or
Postgres needed.
Create an account at http://localhost:7331 — signup is open in dev and
the verification email lands in the local Mailpit (or use the seeded demo
account: uncomment SUPERUSER_EMAIL / SUPERUSER_PASSWORD in
infra/compose/compose/.env before first boot and the migrator seeds
it for you).
Every PR runs bun run validate in each touched app. CI runs the same
script. Local green is CI green.
validate chains:
- typecheck (
tsc --noEmit) - lint (
eslint --max-warnings 0) - unit + integration tests
- build
Plus the cross-cutting workflows:
openapi-drift— fails the PR ifapps/ui/src/lib/api/schema.d.tsis stale against the live API contract. Regenerate withcd apps/ui && OPENAPI_URL=http://localhost:7330/swagger/json bun run generate:api.full-stack-smoke— boots the compose stack and runs the Playwright e2e suite against a freshly migrated DB.
- Signed commits required on
main. Set up signing before pushing. - Squash-merges only. Commit messages on PRs become the squashed commit
message — write them so they read well in
git log. - Conventional prefixes (
feat:,fix:,refactor:,chore:,docs:) are honored by the changelog generator.
The lint config is strict on purpose:
- No
any(useunknown+ narrow) - No
ascasting (onlyas const) - No non-null
! - No floating promises
- No
eslint-disableinline — if a rule fights real intent, editeslint.config.jsand explain why in the PR
Architectural plugins enforce import boundaries, audit-log coverage on
mutating service methods, transactional multi-write paths, sibling tests
for every *.service.ts / *.routes.ts / *.utils.ts, and account
scoping on every query that touches an account-scoped table.
Open an issue at github.com/boringstack-xyz/boringstack/issues. Security issues go through the SECURITY.md flow, not the public tracker.