Skip to content

0001 — Monorepo with three-layer structure ​

Status: Accepted

Date: 2026-06-17

ADO work item: AB#3073

Deciders: Kristopher Turner (platform owner)


Context ​

Heritage Community Hub is one member application delivered as three clients of the same platform API: the web app (apps/web, React + Vite, installable PWA), the iOS app (React Native + Expo, later Epic AB#3077), and the Android app (React Native + Expo, later Epic AB#3139). The web app is part of the Platform Epic (AB#3074) and is the first usable client — it opens directly to the sign-in / register screen at heritageva.app, with no public or marketing site in front of it.

All three clients share a single backend API and a common domain model. Three independent repositories would require maintaining three copies of API contracts, auth logic, and domain types in sync. Any drift between surfaces produces bugs that are hard to trace and expensive to fix.

The project starts with a very small team (one primary developer). Coordination overhead of multiple repos is not justified at this stage. Workspace tooling options evaluated: Turborepo, Nx, plain npm workspaces.

Decision ​

We will keep all application code in one Git repository, organized into three layers via npm workspaces: apps/ (deployable surfaces), packages/ (shared code), and infrastructure/ (IaC). npm workspaces is chosen over Turborepo/Nx to minimize tooling complexity at team size 1–3.

text
heritage-community-hub/
├── apps/
│   ├── api/        # backend — containerized Node.js API (Azure Container Apps)
│   ├── web/        # React + Vite — first client; installable PWA; hosted on Azure SWA (heritageva.app)
│   ├── mobile/     # React Native + Expo — iOS (AB#3077) + Android (AB#3139), later Epics
│   └── landing/    # retired interim placeholder — entry point is apps/web sign-in/register
├── packages/
│   ├── shared-types/    # domain types + API contracts (single source of truth)
│   ├── api-client/      # typed SDK wrapping the API
│   ├── shared-utils/    # validation, formatting, RBAC helpers
│   ├── shared-config/   # shared constants
│   └── ui/              # design tokens + shared design system
├── infrastructure/      # Bicep IaC
├── database/            # schema, migrations, seeds
└── docs/                # ADRs, architecture guides

Note: apps/* and packages/* are planned, not yet scaffolded. Created in Phase 1 (AB#3074). The three clients (web, iOS, Android) all consume the same api-client SDK — no surface holds business logic.

Alternatives considered ​

OptionProsConsWhy not chosen
Monorepo — npm workspaces (chosen)No extra tooling; built into npm/Node; zero learning curveNo build caching; slower CI as repo grows— chosen
Monorepo — TurborepoRemote build caching; task graphAdds dependency; overkill at team size <5Can be added later if CI becomes slow
Monorepo — NxPowerful project graph; code generatorsSteep learning curve; heavy configNot justified at current team size
Multi-repo (one per surface)Clean deploy boundariesType drift risk; cross-cutting PRs require N repos; overhead impractical at <5 devsRevisit only if team hits 10+ devs or divergent stacks

Consequences &ZeroWidthSpace;

Positive &ZeroWidthSpace;

  • A feature implemented once (API endpoint + shared type) is available on web and mobile without duplication.
  • packages/shared-types is the single source of truth for the API contract — web, mobile, and API all import from the same definition.
  • Atomic PRs: an API shape change, web client update, and mobile client update land in one commit.
  • Uniform linting, security scanning, and standards enforcement across the whole platform.

Negative / trade-offs &ZeroWidthSpace;

  • Mobile has a separate release cadence (App Store + Play Store review). Handled via a separate pipeline, not a separate repo (EAS Build in GitHub Actions — see ADR 0004).
  • npm workspaces has no built-in build caching. CI must use path-based filtering (changed-paths filter in GitHub Actions) to avoid rebuilding everything on every commit.

Risks &ZeroWidthSpace;

  • Tight coupling creep — shared code that should go in packages/ ends up duplicated in apps/web and apps/mobile. Mitigation: enforce no direct cross-app imports via ESLint import/no-restricted-paths.
  • CI build time — grows linearly without caching. Mitigation: add Turborepo if CI exceeds 10 min. The workspace structure is already Turborepo-compatible.
  • Revisit trigger — multi-repo becomes defensible at 10+ developers or if web and mobile need genuinely divergent technology stacks.

Update (2026-06-17) — tooling and conventions decided &ZeroWidthSpace;

After a best-practices research pass, the platform owner confirmed the following, which refine the original decision (the monorepo + three-layer structure stands; the tooling specifics are now fixed):

  • Package manager: pnpm (with nodeLinker: hoisted so React Native / Metro resolves workspace packages). Supersedes the original "npm workspaces" choice. Conservative fallback: npm, if Expo EAS Build (which historically assumes Yarn) proves difficult with pnpm — validate the EAS path early.
  • Build orchestrator: Turborepo (free/MIT; local task caching + affected-only CI). Adopted now rather than "later" — it is free and removes the npm-workspaces "no caching" trade-off noted above.
  • Feature code placement: start with feature folders inside each surface (apps/<surface>/src/features/<feature>/); promote a slice to packages/features/* only when it is genuinely shared across web and mobile (avoid premature abstraction).
  • UI mockups: built as real React components in packages/ui + Storybook (not throwaway HTML); the old mockups/ folder is retired. Tracked as ADO AB#3160.
  • Documentation: two buckets — internal repo docs (docs/internal/, not public) and member-facing in-app docs (docs/member/, served in-app after sign-in, including roadmap/changelog). No public docs tier (closed community). See the platform strategy "Documentation model".
  • Cost constraint: every tooling choice must stay free/cheap (all of the above are free).

These decisions are tracked in ADO under AB#3161 and mirrored in pmo/platform-strategy.md.

Three-client model (2026-06-20): the platform has one member application and three clients — web (apps/web), iOS (apps/mobile, AB#3077), and Android (apps/mobile, AB#3139). The web app is the first client, part of Platform Epic AB#3074, and is an installable PWA hosted at heritageva.app. There is no public-facing site or marketing site at any URL. This supersedes any earlier reference to apps/landing as an interim public surface.

Update (2026-06-18) — internal docs organized by document type (Diátaxis reversed) &ZeroWidthSpace;

The docs/internal/ tree was briefly organized by the Diátaxis framework (quadrants: tutorials/, how-to/, reference/, explanation/). This was reversed as unintuitive — design and architecture docs filed under "explanation," build docs under "how-to," etc. did not read like what they were. Internal docs are now organized by what each document is:

  • adr/ — architecture decision records
  • design/ — data model, auth/RBAC, API contracts, security HIGH-item designs
  • architecture/ — system architecture (+ diagram), provider abstraction, notification transport, technical overviews
  • build/ — local development, CI/CD, workspace setup
  • implementation/ — API framework, repository adapters, RBAC implementation
  • operations/ — runbooks (observability, backup/DR, cost governance)
  • overview/ — executive summary, ministry overview, action plan, repository strategy

21 docs + the architecture diagram were moved via git mv (history preserved) and all cross-references updated. Commit 1554ff5.

References &ZeroWidthSpace;

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.