Skip to content
sign in

==> read the decisions

ADR-0015: The platform's code is organised by domain

Status: Accepted (2026-10-14). Implemented 2026-10-15.

The API is one file per concern (session.ts, models.ts, reconcile.ts, settings.ts, ...) sharing one database handle, with routes, rules and SQL interleaved in each. At about 1,700 lines it still reads, but every rule worth a test -- when a session is valid, which door may call which model, what a reconcile converges to -- is reachable only through HTTP and a database.

  • Six bounded contexts, each a directory under api/src/:

    identity User; sign-in, password change
    access Session, ApiToken, Principal (a user plus the door they came through), CSRF,
    Invite; resolve the caller, issue and revoke credentials, last seen
    preferences Settings and its values (ThemeId, Mode, Lang); the resolution ladder
    tenancy Tenant (= user, ADR-0001), TenantDatabase, Release, Project, ServiceHost;
    the registry and forwarding to the user's host
    models Model, ModelGrant, UsageRecord; proxying, gating, usage and per-request metrics
    provisioning Roster (desired state) -> reconcile users, grants, projects, roles, databases
  • Four layers inside each, and dependencies point inward only:

    domain.ts types and rules, pure: no I/O, no framework; this is where the tests are
    service.ts use cases (login, createInvite, revokeSession, recordUsage, reconcile)
    repo.ts Postgres, through Bun.sql
    http.ts Hono routes
  • A small kernel holds what every context needs: the database pool, configuration, typed errors, logging (ADR-0018), and the assembly of the app.

  • Contexts talk through service functions, never through each other's tables.

  • DDD-lite, deliberately: no aggregates framework, no event bus, no CQRS. The point is where a rule lives and that it can be tested without a server; the ceremony would outweigh a codebase this size.

  • Each rule worth a test is a pure function with bun test beside it.
  • The layering is mechanical enough for lint to check (no repo.ts import from domain.ts).
  • Shared code that both the platform and user services need still lives in the shared library; a context is the platform's own.