6. Shared Code Lives in shared/ Folders with Path Aliases, Not Nx or Publishable Packages

Status

Accepted — 2026-02-18 (project inception)

Context

The workspace has cross-package code: models, constants, utility functions, and services used by the frontend, Cloud Functions, and the build tools. The conventional “correct” options at this scale are an Nx monorepo with generated libraries or publishable npm packages in a Lerna-style setup. Both impose meaningful tooling overhead — library project scaffolding, package versioning, build graphs, publish pipelines — and both assume the code will eventually be consumed by something outside this repository.

Decision

Shared code lives in top-level folders (shared/models, shared/constants, shared/utils, shared/services), exposed through TypeScript path aliases in tsconfig.base.json:

  • @jjk/modelsshared/models/index.ts
  • @jjk/constantsshared/constants/index.ts
  • @jjk/utilsshared/utils/index.ts
  • @jjk/servicesshared/services/index.ts
  • @jjk/shared/*shared/*
  • @app/core/*, @app/shared/*, @app/content → frontend-internal paths

npm workspaces declare frontend, tools/build, tools/blog-scheduler, and shared as the workspace members. There is no Angular library project, no publishable package, no build graph.

Consequences

  • Adding shared code is moving a file and exporting it from a barrel. No generator, no build step, no version bump.
  • Imports are stable across the repo. @jjk/models means the same thing from a component, a Cloud Function, or a build-pipeline stage.
  • There is no enforcement of dependency direction between folders. shared/ is by convention leaf code — it must not import from frontend/ or functions/ — and that convention is held by reviewer discipline, not tooling. Drift is possible. (See amendment 2026-04-19.)
  • The code is not publishable. If something in shared/ ever needs to be consumed by a separate repository, it would need to be extracted into a real package at that point.
  • Refactoring a shared export is a repo-wide find-and-replace, not a package version bump. Fast in a single-consumer codebase; would be painful with external consumers.
  • Onboarding a future maintainer who expects Nx is a mismatch; the lack of library tooling is a deliberate cost savings, not an oversight.

Amendment — 2026-04-19

The original Consequences noted that the leaf-code direction was held by reviewer discipline only. That gap is now closed. tools/lint/check-shared-boundaries.ts walks every .ts file under shared/ and fails the build if any module inside shared/ imports from frontend/, functions/, or tools/. shared/ being imported by those packages is the whole point and is unaffected; what the check forbids is shared/ reaching back into its consumers (via @app/* aliases, relative paths that escape shared/, or bare specifiers that collide with workspace directories). The check runs as part of npm run build (and therefore in every CI deploy workflow) and is also runnable locally as npm run lint:boundaries.

See diagrams/dependency-graph.md for the current consumer-to-leaf shape.