// 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──▶ ┐
  scoringScoreSubmitted──▶ ┤
  seasonFlightExtended─▶  ┼──▶ [ event bus ] ──▶ communications ──▶ Slack + in-app feed
  playoffPlayoffSeeded───▶ ┤                       └──▶ 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.

league
Tenants, memberships, roles, fees, invites
scoring
Rounds, submissions, standings, handicaps
season
Seasons, flights, schedules, settings
communications
Slack + in-app, event→message mapping
governance
Extensions, PCC, official proceedings
playoff
Config, seeding, payouts
achievements
Badges, award triggers, recalc
identity
Auth sessions, OAuth, users
contexts/architecture-boundaries.test.ts
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):

contexts/league/api/index.ts
// 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

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 }

league emits, communications subscribes — no direct dependency
// 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

lint typecheck test build knip integration · postgres:16
.github/workflows/ci.yml
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 5
vitest.integration.config.ts
fileParallelism: 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.