Skip to content

10 — Domain Packages

Migrated from root technical docs.

This document defines what belongs in domain packages in this repository and records the first extraction pass from app/feature layers.

Related docs:

  • 02-package-categories.md
  • 03-dependency-rules.md
  • 08-feature-internals.md

Domain packages own reusable business logic that should be framework-light, runtime-light, and easy to test in isolation.

They are used by apps, server functions, and feature packages, but do not own UI composition, transport wiring, or Cloudflare runtime assembly.


  • domain policies and permission rules
  • domain-level normalizers and mappers with business meaning
  • value objects and domain-specific primitive transformations
  • domain service contracts (interfaces) when runtime adapters are needed
  • pure or mostly pure functions suitable for unit testing
  • React components, hooks, route files, or page composition
  • TanStack Query queryOptions, useQuery, useMutation
  • Cloudflare Env, D1/KV binding assembly, worker entrypoints
  • request/response transport parsing and HTTP status wiring

Path: packages/domain-auth/

Owns reusable workspace access policies:

  • canUseAdminWorkspace(isSuperadmin)
  • hasWorkspaceMembership(workspaceMemberships, requiredWorkspaces?)

Current consumers:

  • apps/dashboard/src/app/layouts/WorkspaceSwitcher.tsx (direct import, 2026-04-23)
  • apps/dashboard/src/app/layouts/DashboardLayout.tsx (direct import, 2026-04-23)
  • apps/dashboard/src/lib/routing/middleware.ts

Note: src/app/workspace/access.ts re-export shim deleted 2026-04-23 — consumers now import directly.

Path: packages/domain-people/

Owns reusable people business rules:

  • affiliation status normalization (toAffiliationViewStatus)
  • affiliation policy helpers (isExternalAffiliation, affiliationStatusLabel)
  • membership-filter mapping (affiliationMembershipStatusesForFilter)
  • category-id normalization (normalizeCategoryIds)

Current consumers:

  • apps/dashboard/src/server/functions/people/index.ts
  • apps/dashboard/src/server/functions/people/row-mappers.ts
  • apps/dashboard/src/features/people/pages/PersonDetailPage.tsx
  • apps/dashboard/src/features/people/components/PeopleTable.tsx
  • apps/dashboard/src/features/people/components/person-modal/PersonModalForm.tsx
  • apps/dashboard/src/features/people/components/person-modal/form-utils.ts

  • feature/app layers compose UI and call APIs/queries
  • server functions handle transport, persistence orchestration, and runtime concerns
  • domain packages own reusable business rules and pure normalization

In other words: orchestration stays local; policy/normalization moves to domain.


These remain intentionally outside domain packages in this pass:

  1. People SQL projection and query-shape composition in apps/dashboard/src/server/functions/people/index.ts. Reason: tightly coupled to D1 query structure and pagination.

  2. Person row DB hydration helpers with direct D1 access in apps/dashboard/src/server/functions/people/row-mappers.ts (ensurePersonCategoriesExistOnShard, shard sync, assignment cleanup). Reason: repository/runtime concerns, not pure domain.

  3. Route-level auth redirects and request-context handling in apps/dashboard/src/lib/routing/middleware.ts. Reason: transport/routing orchestration; only reusable policy decisions were extracted.


When adding new business logic:

  1. If logic is reusable and mostly pure, place it in a domain package.
  2. If logic is route/UI/transport/runtime orchestration, keep it local.
  3. If logic needs D1/KV bindings directly, keep implementation in server/worker runtime, but define reusable policy/contract pieces in a domain package.
  4. Prefer incremental extraction by responsibility (policy, normalization, lifecycle transitions), not large all-at-once moves.