// engineering deep-dive
A multi-tenant SaaS built on strict domain boundaries.
Eight bounded contexts. Ports & adapters. A typed event bus. Errors as values, not exceptions. Integration tests against real Postgres — in CI.
Yes, it runs a golf league. The interesting part is how it's built.
- bounded contexts
- 8
- TypeScript source files
- 1,083
- test files
- 499
- migrations
- 51
- boundary violations
- 0
explain architecture --map
league ──LeagueCreated──▶ ┐ scoring ─ScoreSubmitted──▶ ┤ season ─FlightExtended─▶ ┼──▶ [ event bus ] ──▶ communications ──▶ Slack + in-app feed playoff ─PlayoffSeeded───▶ ┤ └──▶ achievements recalc governance ──PccAdjusted────▶ ┘
Each of 8 contexts owns its own domain / api / spi layers (ports & adapters). Cross-context calls go ONLY through the context's api/ barrel; the only other allowed cross-context imports are domain/events and domain/value-objects — the published integration contract. Enforced by src/contexts/architecture-boundaries.test.ts, whose violation allowlist is kept at exactly zero.
const PUBLISHED_LAYERS = ['domain/events', 'domain/value-objects'] as const // Empty: every cross-context reach-in has been paid down. Keep it empty — // any new entry is a fresh boundary violation to FIX, not allowlist. const KNOWN_VIOLATIONS: ReadonlySet<string> = new Set([])
When a cross-context call would create an eager module cycle, the only sanctioned escape is a lazy import at the call site (ADR-0008):
// Lazy import breaks the league↔scoring eager module cycle (ADR-0008). memberHasScoresInSeason: async (lg, sn, m) => { const { memberHasScoresInSeason } = await import('~/contexts/scoring/api/index.ts') return memberHasScoresInSeason(lg, sn, m) }
cat shared-kernel/result.ts
export type Ok<T> = { readonly _tag: 'ok'; readonly value: T } export type Err<E> = { readonly _tag: 'err'; readonly error: E } export type Result<T, E> = Ok<T> | Err<E> export const isOk = <T, E>(r: Result<T, E>): r is Ok<T> => r._tag === 'ok' // callers must narrow before reading .value: const r = await getSeasonByGid(gid) return isOk(r) && r.value ? r.value.entryFeeCents : null
Every command and repository returns Result<T, E> — a discriminated union on _tag, no thrown exceptions across boundaries. Reading .value is only legal after narrowing with isOk(), so the compiler enforces error handling at every call site. Command errors carry a fixed code set — INVALID | NOT_FOUND | CONFLICT | FORBIDDEN | PROVIDER_ERROR — that routes map straight to HTTP.
trace event MemberJoined
command → persist → emit → [ event bus ] → { Slack · in-app feed }
// in a league command: persist, then emit (zero import of communications) await dispatchEvent( leagueCreated({ leagueGid, leagueId, name, slug, createdByUserGid }), ) // communications wires handlers on startup, by event name eventBus.on('MemberJoined', bindHandler(handlers.onMemberJoined)) eventBus.on('FeePaid', bindHandler(handlers.onFeePaid)) eventBus.on('RoundDeclared', bindHandler(handlers.onRoundDeclared))
Handlers run AFTER the DB transaction commits and are idempotent (deduped via unique indexes). One channel-agnostic envelope (ADR-0001) fans a single event out to BOTH Slack and the in-app activity feed — renderers never know the event kind. Async work (GHIN scrapes, fee reminders) is queued on pg-boss; outbound Slack guards re-emitted reminders with dedupeOutboundBySource (the outbound_messages table has no unique constraint).
run ci --verbose
services:
postgres:
image: postgres:16-alpine
env: { POSTGRES_USER: app, POSTGRES_PASSWORD: app, POSTGRES_DB: app }
options: >-
--health-cmd "pg_isready -U app"
--health-interval 10s --health-retries 5fileParallelism: false, // serial against a shared DB sequence: { sequencer: AlphabeticalSequencer }, // identical order → local ≈ CI
Integration tests run against a REAL Postgres 16 service container — migrated fresh each run, executed in deterministic alphabetical order so local and CI behave identically. Stack: React Router 7 (framework mode), Drizzle ORM, Vitest, Biome, tsgo typecheck, Node 26 native ESM (no transpile step). All 51 migrations are generated by drizzle-kit — hand-editing the SQL or journal is forbidden. Internal IDs are ULID gid (never exposed); public IDs are 8-char NanoID id (ADR-0006). pnpm build is itself a guardrail — it fails if server-only code (node:crypto, DB driver) leaks into the client bundle.
Boundaries as a test
A unit test fails on any cross-context reach-in. Allowlist: empty.
Cycle breaker, by ADR
Lazy await import() at the call site is the only sanctioned cycle break (ADR-0008).
Bundle boundary
pnpm build fails if server-only code leaks into the browser bundle.
Dual identifiers
ULID gid internal, NanoID id public — never exposed (ADR-0006).
Generated migrations
51 drizzle migrations, all generated; the journal is load-bearing.
No transpile
Node 26 native ESM + tsgo. --experimental-strip-types.
The dev workflow itself is agent-friendly — work happens in isolated git worktrees, and AGENTS.md pins a ## Verify checklist (pnpm lint · typecheck · test · knip · build) that every change must pass before it's considered done.