Architecture
Monorepo shape
apps/
tracking/ @enode/tracking — session-tracking app (:3000)
portal/ @enode/portal — portal app (:3001)
packages/
core/ @enode/core — API client, BLE, training models, prefetch stores,
hooks, units (the framework-agnostic logic)
ui/ @enode/ui — presentational components + design-system.css
docs/ hand-written Markdown (this site's content)
docs-site/ Docusaurus site + all doc tooling config
scripts/ app.mjs (per-app task dispatcher), build-docs.mjs
Apps depend on @enode/ui, which depends on @enode/core. The shared packages
ship TypeScript source; each app's next.config.ts lists them in
transpilePackages so Next compiles them directly (no separate build step).
Path aliases resolve via the root tsconfig.json:
@enode/core/* → packages/core/src/*, @enode/ui/* → packages/ui/src/*.
Build target
Each app's next.config.ts branches on NODE_ENV:
- Dev — proxies
/api/*to the backend (same-origin, no CORS). - Prod —
output: "export"(fully static). Hard constraint: no SSR, no request-reading Route Handlers, no Server Actions. The static export is what Capacitor wraps for native.
Read path vs. write path
The most important rule (lives in @enode/core):
- Read path → DTOs. Backend return DTOs (
packages/core/src/api/dtos/) are consumed directly as read-only state. Fine for reference data; deferred for workouts. - Write path → domain models. The training write path
(session / set / rep / measurement) uses
packages/core/src/training/. Models cross toSessionCreateDtoonly insidecore'sapi/, at upload time.
See State management.
BLE
Layered behind an adapter so the Web Bluetooth implementation can be swapped for
a Capacitor-native one (packages/core/src/ble/):
types.ts— theBleService/BleConnectionadapter interface.web-bluetooth.ts— the Web Bluetooth implementation (getBleService()).capacitor-enode-sensor.ts— the Capacitor-native path.enode-sensor.ts— sensor command/event class over aBleConnection.enode-sensor-protocol.ts— the pure byte-parsing layer (unit-tested).
Rule: never touch navigator.bluetooth above the adapter. Bytes are normalized
to Uint8Array at the adapter boundary.
Prefetch stores
Reference-data caches in @enode/core (metrics, text-content,
user-app-settings, …): each is a module-level store + useSyncExternalStore
hooks + an <XLoader/> mounted in each app's layout.tsx.
TODO: AGENTS.md notes a planned "collapse the prefetch stores into one generic factory" — verify against the current
packages/core/src/*/store.tsbefore relying on it.