Skip to content

Architecture Discovery, Documentation & Live Mapping Pipeline ​

Context ​

Heritage Community Hub has grown to 40 database tables, 20 API feature routers, two front-end apps (web + mobile), and 7 Azure resources, but its architecture documentation is hand-maintained and out of date. data-model-design.md covers only the earliest tables; there is one hand-drawn draw.io topology, no ERD, no API spec, and no automated guard against the recurring bad-import / hardcoded-URL / undocumented-table class of bug.

The goal is a pipeline that discovers everything, generates per-file/per-table docs, and maintains a near-live visual map that auto-updates on every push — at zero new monthly cost.

Decisions already locked:

  • Four layers: runtime topology + auto-gen diagrams + per-file/table docs + drift CI gate
  • Open-source + Azure-native only; no new paid SaaS
  • Publish to MkDocs Material on Azure Static Web App, rebuilt by CI on each push

What already exists — leverage, don't rebuild ​

AssetLocationStatus
App Insights SDK; cloudRole = "Heritage API"apps/api/src/lib/telemetry.ts:40–42✅ done
Web App Insights; cloudRole = "Heritage Web"apps/web/src/lib/analytics.ts:25–26✅ done — draft plan said "add this"; it's already there
Application Map dashboard tile (AppMapGalPt)infrastructure/modules/observability.bicep:181✅ provisioned
Prisma schema (40 models)apps/api/prisma/schema.prismasource for ERD + table docs
Zod schemas per feature (20 feature routers)apps/api/src/features/*/*.schema.tssource for OpenAPI (phase 3)
ESLint provider-SDK boundary rule.eslintrc.cjs:17–43seed for dependency-cruiser
gitleaks + block-secrets hookci.yml, .claude/hooks/block-secrets.ps1extend for hardcoded-ref lint
Turbo tasks: build, test, lint, dev, cleanturbo.jsonadd docs:arch
CI: lint-and-build, test, secret-scan, snyk.github/workflows/ci.ymladd build-arch-docs job

Pipeline shape ​

┌──────────────────────────────────────────────────────────────┐
│  git push to main                                            │
└──────────────┬───────────────────────────────────────────────┘
               │ triggers
       ┌───────▼────────┐
       │ build-arch-docs │  (new CI job)
       │  GitHub Action  │
       └───────┬─────────┘
               │ runs sequentially
    ┌──────────▼───────────────────────────────────────────┐
    │  1. pnpm --filter @hch/api exec prisma generate      │  → ERD + table docs
    │  2. npx depcruise apps packages --output-type mermaid │  → import graph
    │  3. npx typedoc (packages/ + apps/api)               │  → type reference
    │  4. (phase 3) copy openapi.json from /docs/json      │  → API spec
    │  5. mkdocs build                                      │  → static HTML
    └──────────▼───────────────────────────────────────────┘

       ┌───────▼─────────────────┐
       │  Azure Static Web App   │  (free tier — mirrors deploy-web pattern)
       │  docs-site/             │
       └─────────────────────────┘

Also wired into the existing lint path in CI: dependency-cruiser validation fails the lint-and-build job fast on forbidden imports, so bad-link feedback lands on PRs without waiting for the full build-arch-docs job.


Layer 1 — Live runtime topology (Azure App Insights Application Map) ​

Code work: minimal. Both cloudRole tags are already set. The Application Map tile is already provisioned. This layer is operational, not code.

Remaining actions (no code change):

  • Verify APPLICATIONINSIGHTS_CONNECTION_STRING is populated in the prod Container App secret. This overlaps existing AB#4542. Note it as a prerequisite; don't duplicate work.
  • Optionally: label outbound SDK dependency spans for Clerk / YouTube / Blob in the adapter files so they read "Clerk" / "YouTube" / "Azure Blob" on the map rather than raw hostnames. This is a low-priority polish item — App Insights auto-labels by hostname already.

Deliverable: a short runbook paragraph in the MkDocs site pointing to the Application Map URL and explaining how to read it. No diagram to maintain.


Layer 2 — Auto-generated design diagrams ​

2a. ERD (phase 1) ​

Add two generator blocks to apps/api/prisma/schema.prisma after the existing prisma-client-js generator:

prisma
generator erd {
  provider = "prisma-erd-generator"
  output   = "../../docs/internal/architecture/generated/erd.svg"
  theme    = "default"
  mermaidErd = true
}

generator docs {
  provider = "prisma-docs-generator"
  output   = "../../docs/internal/architecture/generated/prisma-docs"
}

Add dev dependencies to apps/api/package.json:

prisma-erd-generator  @mermaid-js/mermaid-10
prisma-docs-generator

Running pnpm --filter @hch/api exec prisma generate will then emit both the ERD SVG/MD and the per-model HTML docs into docs/internal/architecture/generated/.

2b. Module dependency graph (phase 1) ​

New root-level .dependency-cruiser.cjs with rules that:

  • Fail on web/mobile importing API internals
  • Fail on any app importing another app's internals
  • Port the existing ESLint provider-SDK boundary zones (.eslintrc.cjs:17–43) as dependency-cruiser forbidden rules so both tools enforce the same boundary
  • Fail on circular dependencies
  • Warn on orphan modules

Add dependency-cruiser to root devDependencies. Add a depcruise script and wire it into the turbo lint task so pnpm turbo lint catches forbidden imports on PRs.

Emit the import graph SVG artifact:

bash
npx depcruise apps packages --output-type mermaid > docs/internal/architecture/generated/deps.mmd

2c. OpenAPI spec (phase 3 — after phase 1 ships) ​

Register @fastify/swagger in apps/api/src/app.ts after the helmet/cors/rateLimit registrations, before the feature routers. Route schemas come from the existing Zod schemas via fastify-type-provider-zod. The spec is exposed at /docs/json.

This is additive — it must not change runtime behavior of the 20 existing routers.


Layer 3 — Per-file / per-table reference docs ​

  • Tables: prisma-docs-generator (Layer 2a) outputs browsable per-model HTML
  • API: render the Layer 2c OpenAPI JSON with mkdocs-render-swagger-plugin in MkDocs
  • Code/types: TypeDoc over packages/shared-types, packages/api-client, packages/ui (add typedoc to root devDependencies; output to docs/internal/architecture/generated/typedoc/)

Layer 4 — Drift detection CI gate ​

4a. Dependency boundary (phase 1, blocking lint) ​

.dependency-cruiser.cjs rules fail CI. Already described in Layer 2b.

4b. Hardcoded-reference lint (phase 4) ​

New script scripts/check-hardcoded-refs.ts (tsx, ~40 lines) that:

  • grep -rE 'https://api\.heritageva\.app|https://.*\.clerk\.com' over apps/ excluding *.test.ts, *.spec.ts, node_modules/, dist/
  • Exits 1 with a clear message if found (should come from env vars)
  • Wired as a step in build-arch-docs CI job (or as its own lint step)

4c. Schema-drift check (phase 4) ​

New script scripts/check-schema-drift.ts that:

  • Reads apps/api/prisma/schema.prisma, extracts all model <Name> declarations
  • Reads docs/internal/design/data-model-design.md, extracts ### headers
  • Prints any models not covered by a matching section header
  • Exits 1 (blocking) when undocumented model count > 0

Publish — MkDocs Material on Azure SWA &ZeroWidthSpace;

New files &ZeroWidthSpace;

docs-site/mkdocs.yml — MkDocs config that:

  • Uses mkdocs-material theme
  • Includes mkdocs-mermaid2-plugin (renders the ERD + deps graph Mermaid blocks)
  • Includes mkdocs-render-swagger-plugin (renders OpenAPI spec; phase 3)
  • Nav tree: Architecture Overview → ERD → Dependency Graph → API Spec → TypeDoc → ADRs

docs-site/docs/ — aggregation layer that pulls from:

  • docs/internal/architecture/generated/ (ERD, deps graph, TypeDoc, OpenAPI)
  • docs/internal/adr/ (existing ADR markdown)
  • docs/internal/architecture/*.md (existing prose)
  • docs/internal/design/*.md (existing design docs)
  • A new live-topology.md runbook page linking to the Azure Application Map

CI job &ZeroWidthSpace;

Add build-arch-docs job to .github/workflows/ci.yml:

yaml
build-arch-docs:
  runs-on: ubuntu-22.04
  needs: lint-and-build
  steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v4
    - uses: actions/setup-node@v4
      with: { node-version: '20', cache: 'pnpm' }
    - run: pnpm install --frozen-lockfile
    - run: pnpm --filter @hch/api exec prisma generate   # ERD + table docs
    - run: npx depcruise apps packages --output-type mermaid > docs/internal/architecture/generated/deps.mmd
    - run: npx typedoc
    - run: pip install mkdocs-material mkdocs-mermaid2-plugin mkdocs-render-swagger-plugin
    - run: mkdocs build --config-file docs-site/mkdocs.yml --site-dir site/
    - uses: Azure/static-web-apps-deploy@v1   # free tier; mirrors deploy-web pattern
      with:
        azure_static_web_apps_api_token: ${{ secrets.DOCS_SWA_DEPLOYMENT_TOKEN }}
        action: upload
        app_location: docs-site/site
        skip_app_build: true

Also add to turbo.json:

json
"docs:arch": {
  "dependsOn": ["^build"],
  "outputs": ["docs/internal/architecture/generated/**", "docs-site/site/**"],
  "cache": true
}

Governance &ZeroWidthSpace;

ADR 0046docs/internal/adr/0046-architecture-documentation-and-mapping.md
Records layered-pipeline decision, tool choices (open-source/Azure-native over SaaS), and amends ADR 0005 (observability) and references ADR 0027/0031 (design tooling).

Generated artifacts committed under docs/internal/architecture/generated/ (ERD, deps graph, OpenAPI JSON) so they are diffable in PRs. Mark the folder linguist-generated: true in .gitattributes to suppress diffs in GitHub UI.


Phasing — each phase independently shippable &ZeroWidthSpace;

PhaseScopeRisk
1 — Quick winsLayer 2a ERD (prisma-erd-generator), Layer 2b dependency-cruiser (blocking lint), Layer 4a deps gate, Turbo docs:arch task, docs/internal/architecture/generated/ directory stubLow — additive tooling only; zero app code changes
2 — Publishdocs-site/mkdocs.yml, CI build-arch-docs job, SWA deploy, ADR 0046, live-topology runbook pageLow — separate from app CI path
3 — API specLayer 2c @fastify/swagger in app.ts (all 20 routers), Layer 3 Redoc + TypeDoc, prisma-docs-generator in MkDocsMedium — touches app.ts and 20 routers additively
4 — Drift gatesLayer 4b hardcoded-ref lint, Layer 4c schema-drift script, promote to CI-blockingLow — scripts only

Critical files to add or modify &ZeroWidthSpace;

FileChange
apps/api/prisma/schema.prismaAdd prisma-erd-generator + prisma-docs-generator generator blocks
apps/api/package.jsonAdd prisma-erd-generator, @mermaid-js/mermaid-10, prisma-docs-generator to devDependencies
.dependency-cruiser.cjsNew — boundary + circular + orphan ruleset (port .eslintrc.cjs zones)
turbo.jsonAdd docs:arch task
.github/workflows/ci.ymlAdd build-arch-docs job; add depcruise step to lint-and-build
docs-site/mkdocs.ymlNew — MkDocs Material config
docs-site/docs/New — aggregation symlinks/copies into mkdocs nav
docs/internal/architecture/generated/New — output dir for ERD, deps graph, TypeDoc, OpenAPI (committed)
.gitattributesAdd docs/internal/architecture/generated/** linguist-generated=true
docs/internal/adr/0046-architecture-documentation-and-mapping.mdNew — ADR
docs/internal/architecture/live-topology.mdNew — Application Map runbook page
scripts/check-hardcoded-refs.tsNew — phase 4, exits 1 on hardcoded API/Clerk URLs
scripts/check-schema-drift.tsNew — phase 4, exits 1 when Prisma models lack doc coverage
apps/api/src/app.tsPhase 3: register @fastify/swagger before feature routers

Verification &ZeroWidthSpace;

  • Layer 1: Open dash-hch-prod-eus in Azure Portal → Application Map tile → confirm nodes for "Heritage API", "Heritage Web", PostgreSQL, Blob, Clerk appear and update after generating live traffic. No code change needed.
  • Layer 2 ERD: pnpm --filter @hch/api exec prisma generateerd.svg exists in docs/internal/architecture/generated/ and contains all 40 model names.
  • Layer 2 deps graph: npx depcruise apps packages --output-type mermaid → renders without errors; introduce a deliberate forbidden import on a scratch branch → CI fails.
  • Layer 3 TypeDoc: npx typedoc builds without errors; output appears in MkDocs site.
  • Layer 4 drift check: add a model Foo to schema without updating data-model-design.mdcheck-schema-drift.ts exits 1 naming "Foo".
  • Publish: CI build-arch-docs job deploys the MkDocs site to the SWA URL; ERD, deps graph (phase 1+2), API spec (phase 3), and TypeDoc are all browsable.
  • End-to-end "live": merge a new model to main → next CI run updates the ERD, table docs, and MkDocs site automatically with no manual diagram editing.

Cost &ZeroWidthSpace;

$0 new monthly. Every generator is open-source. App Insights + Log Analytics already provisioned. The docs SWA is the Azure Free tier (mirrors existing deploy-web SWA pattern in deploy.yml).

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