Appearance
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 ​
| Asset | Location | Status |
|---|---|---|
| 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.prisma | source for ERD + table docs |
| Zod schemas per feature (20 feature routers) | apps/api/src/features/*/*.schema.ts | source for OpenAPI (phase 3) |
| ESLint provider-SDK boundary rule | .eslintrc.cjs:17–43 | seed for dependency-cruiser |
| gitleaks + block-secrets hook | ci.yml, .claude/hooks/block-secrets.ps1 | extend for hardcoded-ref lint |
| Turbo tasks: build, test, lint, dev, clean | turbo.json | add docs:arch |
| CI: lint-and-build, test, secret-scan, snyk | .github/workflows/ci.yml | add 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_STRINGis 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-generatorRunning 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-cruiserforbiddenrules 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.mmd2c. 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-pluginin MkDocs - Code/types: TypeDoc over
packages/shared-types,packages/api-client,packages/ui(addtypedocto root devDependencies; output todocs/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'overapps/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-docsCI 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 allmodel <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 ​
New files ​
docs-site/mkdocs.yml — MkDocs config that:
- Uses
mkdocs-materialtheme - 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.mdrunbook page linking to the Azure Application Map
CI job ​
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: trueAlso add to turbo.json:
json
"docs:arch": {
"dependsOn": ["^build"],
"outputs": ["docs/internal/architecture/generated/**", "docs-site/site/**"],
"cache": true
}Governance ​
ADR 0046 — docs/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 ​
| Phase | Scope | Risk |
|---|---|---|
| 1 — Quick wins | Layer 2a ERD (prisma-erd-generator), Layer 2b dependency-cruiser (blocking lint), Layer 4a deps gate, Turbo docs:arch task, docs/internal/architecture/generated/ directory stub | Low — additive tooling only; zero app code changes |
| 2 — Publish | docs-site/mkdocs.yml, CI build-arch-docs job, SWA deploy, ADR 0046, live-topology runbook page | Low — separate from app CI path |
| 3 — API spec | Layer 2c @fastify/swagger in app.ts (all 20 routers), Layer 3 Redoc + TypeDoc, prisma-docs-generator in MkDocs | Medium — touches app.ts and 20 routers additively |
| 4 — Drift gates | Layer 4b hardcoded-ref lint, Layer 4c schema-drift script, promote to CI-blocking | Low — scripts only |
Critical files to add or modify ​
| File | Change |
|---|---|
apps/api/prisma/schema.prisma | Add prisma-erd-generator + prisma-docs-generator generator blocks |
apps/api/package.json | Add prisma-erd-generator, @mermaid-js/mermaid-10, prisma-docs-generator to devDependencies |
.dependency-cruiser.cjs | New — boundary + circular + orphan ruleset (port .eslintrc.cjs zones) |
turbo.json | Add docs:arch task |
.github/workflows/ci.yml | Add build-arch-docs job; add depcruise step to lint-and-build |
docs-site/mkdocs.yml | New — 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) |
.gitattributes | Add docs/internal/architecture/generated/** linguist-generated=true |
docs/internal/adr/0046-architecture-documentation-and-mapping.md | New — ADR |
docs/internal/architecture/live-topology.md | New — Application Map runbook page |
scripts/check-hardcoded-refs.ts | New — phase 4, exits 1 on hardcoded API/Clerk URLs |
scripts/check-schema-drift.ts | New — phase 4, exits 1 when Prisma models lack doc coverage |
apps/api/src/app.ts | Phase 3: register @fastify/swagger before feature routers |
Verification ​
- Layer 1: Open
dash-hch-prod-eusin 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 generate→erd.svgexists indocs/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 typedocbuilds without errors; output appears in MkDocs site. - Layer 4 drift check: add a
model Footo schema without updatingdata-model-design.md→check-schema-drift.tsexits 1 naming "Foo". - Publish: CI
build-arch-docsjob 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 ​
$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).