Skip to content

Architecture

Daniel Hokanson edited this page Aug 30, 2026 · 4 revisions

Canonical reference: docs/architecture.md, docs/libraries.md, docs/coding-standards.md.

The stack

Layer Technology Container
Frontend Angular + Angular Material — zoneless, signals, standalone components; served by nginx forge-ui
Backend .NET Web API — MediatR/CQRS, FluentValidation, Mapperly, Serilog, Hangfire, SignalR forge-api
Database PostgreSQL + pgvector; schema owned by forge-db forge
Object storage MinIO (S3-compatible) forge-storage
Backups Scheduled pg_dump sidecar forge-backup
Optional Ollama (AI), Coqui (TTS), DocuSeal (signing), Seq (logs), GlitchTip (crash reports) profile-gated

Also in the mix: OpenAPI + Scalar for API docs, Three.js for inline STL rendering, Playwright and Vitest for tests.

Backend layering

forge-api is layered with a genuinely pure core — forge.core has zero project references and holds only entities, enums, interfaces, models and settings.

forge.core          entities / enums / interfaces / models / settings   (no project refs)
forge.data          EF Core DbContext + persistence          → core
forge.integrations  external providers (accounting, shipping, AI, …)  → core
forge.api           features, controllers, capability gates  → the bulk of the code
forge.tests         unit + integration + architecture tests

Features are MediatR request/handler pairs under Features/; controllers are thin. Mapping is source-generated with Mapperly — no runtime reflection mappers. Background work runs on Hangfire with the PostgreSQL storage provider.

Schema ownership: forge-db, not EF migrations

This is the single most important architectural fact to know before touching the database.

There are no EF Core migrations. The desired-state schema is a tree of one-object-per-file SQL scripts in forge-db, reconciled onto a live database with stripe/pg-schema-diff. EF Core is present purely as the query-mapping layer.

  • Schema changes = edit the forge-db schema tree, then regenerate the SQL the API embeds.
  • The API's SchemaBootstrapper provisions a fresh database from that assembled schema and is a no-op on an existing one — it does not reconcile drift.
  • Upgrades of a populated install go through the forge-deploy schema reconcile step.
  • Consequence for developers: a stale local database volume will not self-heal. Reconcile it with the forge-db harness rather than expecting startup to fix it.

The reason for the split is that some of the schema cannot be expressed by an ORM migration generator at all — the vector-search extension behind document search, and the database triggers that make posted accounting entries immutable.

The IClock rule

Time is injected, never read from the ambient clock. IClock (forge.core/Interfaces/IClock.cs) has a SystemClock for production and a SimulationClock for end-to-end tests, which is what makes deterministic multi-week simulation runs possible. It is injected into AppDbContext's timestamp stamping and into time-dependent handlers. New code calling DateTime.UtcNow directly is a standards violation.

Authentication

ASP.NET Identity with JWT bearer tokens for the SPA and refresh-token rotation. Roles are additive — a user can hold several.

Authentication is tiered, because a shop floor and an office need different things:

Tier Method Where
1 RFID/NFC scan + PIN Kiosk (primary)
2 Barcode scan + PIN Kiosk (fallback)
3 Username + password Desktop / mobile
4 Enterprise SSO — Google, Microsoft, generic OIDC Optional

Admins create accounts and issue a setup token; the employee completes their own password, PIN and profile. An admin never views or sets a password or PIN — a reset issues a new setup token instead.

OAuth tokens for the accounting provider are held on a single company-level connection and encrypted via the ASP.NET Data Protection API, with keys in Postgres.

Real-time and background work

SignalR carries the live surface — board sync, notifications, timers and chat run over dedicated hubs, with reconnection handling and a connection banner in the UI. Hangfire runs the scheduled and deferred work.

Pluggable accounting

IAccountingService is a common interface over customers, invoices, estimates, POs, payments, time activities, employees, vendors and items. AccountingServiceFactory resolves the active provider from settings. Each provider owns its auth flow, API client and DTO mapping; the sync queue, caching and orphan detection are provider-agnostic. See Accounting Modes — the behavioural consequences are large enough to deserve their own page.

Standards are enforced, not just written down

Forge's coding standards are backed by tests and lint rules rather than prose alone, using a consistent pattern: hard rules fail the build; legacy debt is held by a per-file ratchet. New files must be clean, already-baselined files may not get worse, and improving a file fails the check until you regenerate and commit the baseline in the same commit. Baselines are never hand-edited upward.

  • forge-api — architecture tests under forge.tests/Architecture/ cover capability gating on controllers and source standards (clock usage, try/catch in controllers, file size).
  • forge-uinpm run lint:standards covers console logging, hardcoded colours, unjustified !important, inline templates and raw form controls.

Before pushing UI work, run the local gates: npm run lint && npm run lint:i18n && npm run test -- --watch=false. Translations live only at public/assets/i18n/{en,es}.json with enforced 1:1 parity between locales.

Clone this wiki locally