Skip to main content

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).
  • Prodoutput: "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 to SessionCreateDto only inside core's api/, 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 — the BleService / BleConnection adapter 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 a BleConnection.
  • 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.ts before relying on it.