Overview
What @workspace/training is, how its domain and application layers are organised, which sub-paths exist, and where each module is documented.
@workspace/training is the product model of Bridge Training: teachers, students, groups, sessions, boards, collections, plans and entitlements, ads, telemetry and usage. It is a pure-TypeScript hexagonal core: the domain layer holds entities, value objects and pure rules; the application layer holds services, input DTOs, query objects and the ports that infrastructure adapters implement. Nothing in the package touches a database, a queue or HTTP.
Runtime dependencies: @workspace/bridge (deal types and rules), @workspace/rich-text (comment and review model), @workspace/shared (logger, time and error helpers), @workspace/i18n and @workspace/i18n-messages (languages and theme labels). No React, no DOM, no vendor SDK.
Quick start
A service is a factory taking its ports; the consuming app passes an adapter and threads a ServiceContext as the last argument of every call.
// Adapters (packages/database) implement the ports; the app wires them once.
export function listGroups(
deps: GroupsServiceDeps,
ctx: ServiceContext
): Promise<Group[]> {
const groups = createGroupsService(deps);
return groups.list(ctx); // ctx is always the last argument
}Domain errors are AppError subclasses carrying a stable code; the API translates them at its boundary, the app never sees them.
export async function renameGroup(
deps: GroupsServiceDeps,
groupId: string,
name: string,
ctx: ServiceContext
) {
const groups = createGroupsService(deps);
try {
return await groups.update(groupId, { name }, ctx);
} catch (error) {
if (isAppError(error)) {
error.code; // => "GROUP_NOT_FOUND"
error.isExpected; // => true
}
throw error;
}
}See Usage for the wiring in each app, Service Context for the ctx rule and Errors for the error family.
Sub-path exports
Every symbol is reached through one of the sub-paths of the package exports map. A module's index.ts is its public surface; the other files of the folder are internal.
| Import path | Contents | Page |
|---|---|---|
@workspace/training | Only the cross-cutting part of domain/shared: the AppError family and ErrorCode, AppEvent, ServiceContext, supported languages, localized fields, calendar and month helpers, label sorting | Errors, Shared, Service Context |
@workspace/training/domain/<module> | The module's entities, value objects, constants, guards, pure functions and domain errors | The module page |
@workspace/training/application/<module> | The module's services (createXxxService), input DTOs, query objects and projections, and its repository ports under ports/ | The module page |
@workspace/training/application/shared | The query layer every repository port is built from: QueryObject, Criteria, Projection, Aggregation, pagination, ReadRepository, WriteRepository | Queries |
@workspace/training/application/ports | Cross-cutting ports: queue, event bus, email, auth admin, storage, session tokens, webhook verification, current account | Ports |
@workspace/training/factories | One (faker, overrides) factory per type, for tests, mocks and dev scenarios | Factories |
@workspace/training/testing | Fakes and mocks of the ports for the consumers' tests | Testing |
Rule: a module is imported through its own path; there is no forwarding file. Inside the package, imports are direct relative paths, never the root barrel.
Modules
Each page documents the domain/<module> and application/<module> folders of one module.
Teaching
| Module | Contents | Page |
|---|---|---|
accounts | Accounts (persons and organisations), owners, roles, preferences, onboarding, identity snapshots from the auth provider, impersonation log | Accounts |
students | Students of a teacher and their group counts | Students |
groups | Groups of students with a schedule, their next session | Groups |
sessions | Group sessions, occurrences of recurring groups, session codes, public links and public content, linked collections | Sessions |
boards | Boards (a deal with teaching metadata): codes, levels, scoring, writing through the gate, claiming, queries and filters, comments, analytics, bookmarks and votes | Boards |
editor-boards | The editor's view of boards: list queries, projections, rotation, prepared writes | Editor boards |
themes | Board themes, system and user-defined, their labels and the query object family | Themes |
collections | Deal collections, governed by the stock ceiling of the plan | Collections |
sharing | Sharing modes and sharing groups between teachers | Sharing |
public | Deal views and the reads served to the public player without a context | Public |
Plans and billing
| Module | Contents | Page |
|---|---|---|
entitlements | Features and limits, grants and their merge over time, flow quotas, diffusion debits, stock overflow and the degraded mode | Entitlements |
plans | Plans, plan grids, prices and the catalogue provider | Plans |
subscriptions | Subscriptions, provider events, reconciliation into grants, termination, admin views | Subscriptions |
billing | Billing customers, hosted checkout and portal sessions | Billing |
Platform
| Module | Contents | Page |
|---|---|---|
ads | Ad banners, campaigns, sponsorships and their rules, the ads policy resolver, brand assets | Ads |
announcements | Announcements and the per-account feed | Announcements |
events | Ingested webhook events: statuses, ingestion, replay, handler registry | Events |
telemetry | Player telemetry events, trace tokens and decoding, ingest with serve debit | Telemetry |
usage | Usage statistics and analytics: serves, play rollups, perceived difficulty, perseverance, owner activity | Usage analytics |
Architecture
Layer first, module second: the lint rule is the layer rule, domain ↛ application ↛ infrastructure. Modules are too intertwined (a session links boards and collections, entitlements govern collections) to be the first boundary.
packages/training/src/
├── domain/
│ ├── shared/ # AppError family, ErrorCode, ServiceContext, languages, localized fields
│ └── <module>/ # entities, value objects, errors, pure rules
├── application/
│ ├── shared/ # QueryObject, Criteria, Projection, pagination, Read/WriteRepository
│ ├── ports/ # cross-cutting ports (queue, event bus, email, storage, tokens…)
│ └── <module>/ # services, input DTOs, query objects
│ └── ports/ # the module's repositories
├── factories/ # (faker, overrides) factories, one per type
└── testing/ # fakes and mocks of the ports- The layer rule is linted. A
domainfile imports sibling domain modules only; anapplicationfile imports domain and application; neither imports an infrastructure or UI package, the package's own public entry points, orfactories/outside tests. - Primitives imported, product model injected.
bridge,rich-textandsharedare imported directly by anyone.trainingis reached by infrastructure adapters through its ports, and by the UI through ViewModels built in the apps: noui-*package imports it. - Services are plain factories.
createXxxService(deps)returns an object of methods; every method acting on a user's data takesctx: ServiceContextlast. Authorization is decided by the app before the call. - One regime per service. A service either calls repositories request by request or runs a unit of work; it never mixes the two.
- Errors are data. Services throw
AppErrorsubclasses with anErrorCode; the API handlers translate them into wire errors.
The package rules shared with the rest of the monorepo are on Architecture and Adapter packages.