Skip to content

Developer Setup

Daniel Hokanson edited this page Aug 30, 2026 · 1 revision

This page is the development inner loop: how to get a Forge stack running from source with hot reload, what stops a first boot, how the repos sit on disk, and which test suites need what. Installation is the production path and ./setup.sh --source is a production-shaped build from your checkouts — neither of them rebuilds on save. Contributing covers process: where to file, branch model, and the gates to run before you push.

Two loops, pick one

Containerised hot reload is the closer match to how Forge actually runs. From forge-deploy (or from the umbrella checkout, which carries linked copies of the overlays):

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

The docker-compose.dev.yml overlay swaps in the development Dockerfile for both applications, mounts your working tree into the containers, and runs each app under its own watcher — ng serve with poll-based file watching for the UI, dotnet watch with polling watchers for the API. Polling rather than inotify is deliberate: bind-mounted host filesystems do not reliably deliver native change events into a container. The overlay's own header suggests setting COMPOSE_FILE in .env for a persistent dev mode; setup.sh rewrites that variable whenever it runs, so the explicit two--f form is the habit that survives.

The overlay also re-exposes UI, API, Postgres and object storage on every interface, so you can point a phone or a tablet on the same network at your laptop. The base compose file binds all of them to loopback through per-service *_BIND variables. That difference is the whole point of the overlay, and it is also why it belongs on a trusted network only — if you need the hot-reload loop on a machine that is reachable more widely, set the *_BIND variables back to 127.0.0.1 in .env and reach it over an SSH tunnel instead. See Hardening a Production Install for the production posture.

Host-native is faster to attach a debugger to. Run the API with the .NET SDK version pinned in forge-api/global.json, pointed at a Postgres you started yourself, then npm start in forge-ui. Per-repo commands are in each sibling repo's own CONTRIBUTING.md; they are the canonical copy and they change.

One thing that confuses people in both loops: the development SPA talks to the API by absolute URL, configured in src/environments/environment.ts, not through the dev server's proxy. proxy.conf.json is wired into the serve target and routes /api, /hubs and the signing service to compose service names, but nothing exercises it while the environment file names an absolute host — so a proxy edit that appears to do nothing is doing exactly nothing. Change the environment file instead. (Production builds swap in environment.prod.ts; the native mobile shell is its own build configuration again — see Mobile and Offline.)

Two things that stop a first boot

Both fail early and neither is guessable from the error alone.

The API refuses to start without a JWT signing key of at least 32 characters. Nothing ships one — there is no committed fallback, on purpose — so supply Jwt__Key (or the JWT_KEY environment variable the compose stack reads) yourself. setup.sh generates one into .env for you; a host-native run does not get that for free.

Postgres must be a pgvector image. The declarative schema creates the vector extension on a fresh database, so a stock postgres image fails during schema bootstrap rather than at query time. The compose stack already uses a pgvector image; a hand-started container for host-native work must too.

Repo layout after bootstrap

./bootstrap.sh in the umbrella repo clones the sibling repos as children of the umbrella checkout, not as peers of it. That is deliberate — the compose build contexts resolve relative to that layout — and it is the one place the umbrella CONTRIBUTING.md is misleading. forge-db is not among the cloned repos: if you are touching the schema, clone it separately. Schema changes flow through forge-db as desired-state SQL, never through EF Core migrations; see Architecture § Schema ownership.

Bootstrap then runs a relink step that hard-links the four compose overlay files from forge-deploy into the umbrella checkout and symlinks the tools directory. forge-deploy holds the canonical copy, so make overlay edits there. The catch is that an editor which saves atomically replaces the file rather than writing through the inode, which silently breaks the link and lets the two copies drift — an edit that "does not take effect" is usually this. A CI job in the umbrella repo byte-compares the pair on every pull request; when it fails, re-run scripts/relink.sh rather than hand-syncing the copy. scripts/check-overlay-parity.sh runs the same comparison locally.

The base docker-compose.yml is intentionally not linked. The two copies use different relative build-context paths on purpose, so that either directory works as a working directory.

Running the tests

Four suites, three different requirements.

Suite Needs Docker What it is
forge-api unit + endpoint No The test host swaps EF Core for the in-memory provider, replaces Hangfire storage with memory storage, strips the external health checks and forces the mock-integration posture
forge-api database-backed Yes (or the escape hatch below) A throwaway pgvector container per test collection, with the same declarative schema the application boots from
forge-ui unit No Vitest on jsdom over src/**/*.spec.ts
forge-ui Playwright A running stack No webServer in the config — it assumes the app is already up at the configured base URL

The database-backed suite is not redundant with the in-memory one. It exists because a real Postgres is the only thing that can observe filtered unique indexes, set-based updates, and the accounting-ledger immutability triggers — the in-memory provider models none of them, so the bugs those tests were written for are invisible without it. That is why they cannot be faked away.

The escape hatch, documented nowhere but an XML comment on the fixture: if the Testcontainers client cannot reach your Docker socket — a sandbox that proxies the docker CLI but blocks the raw socket, or a uid outside the docker group — set FORGE_TEST_PG to a connection string for a pgvector Postgres you started yourself and the fixture connects to that instead of starting a container.

FORGE_TEST_PG="Host=localhost;Port=<port>;Database=<db>;Username=<user>;Password=<password>" dotnet test

Two filtering notes. There are no xUnit category traits, so --filter "Category=Unit" matches nothing; filter by the Architecture namespace to get the standards and capability-gate tests. And the Playwright config splits a gating functional project from a non-gating project that only produces screenshots, docs and audits — the artifact generators are memory-hungry and run on their own schedule, so a red run there is not a red gate. The project's testing philosophy is in testing-strategy.md; the pre-push gate list, which is broader than "run the tests", is on Contributing.

First-day gotchas

Everything external returns canned data. The mock-integration posture is on by default in Development, which is why accounting, shipping, e-signing and AI all appear to work with no credentials configured — they are returning fixtures. The API logs a loud readiness warning if that posture is ever active in Production. What a real install has to configure, and how to read the integration readiness report, is on Configuration and Integrations.

An unexpected 429 means you are not in Development. Rate limiting is switched off entirely in the Development environment; elsewhere loopback and infrastructure paths are exempt but ordinary callers are not. Getting throttled locally is a signal that ASPNETCORE_ENVIRONMENT is not what you think. The same variable also decides whether the interactive OpenAPI reference is served at all — see API Access.

Host-native development treats your local database as disposable. The shipped development settings force a recreate on start, so the API drops and reapplies the schema every boot. The compose stack passes the flag explicitly as false, so the containerised loop keeps its data — but a dotnet run on the host will not, and that is by design rather than a bug to report.

A translation rendering as a raw dotted key is almost always the wrong directory. Locale files bundle from the public assets path only. A parallel path under src/assets is deliberately absent, is gitignored, and hard-fails the i18n lint if it reappears, because edits there compile, build and unit-test green while never reaching the bundle. The two shipped catalogs are also key-for-key parity-enforced: a new English key without its counterpart fails the gate. Runtime label overrides are a separate mechanism entirely and do not touch these files — see Customizing an Install.

Read the startup logs before anything else. The API emits a labelled database-lifecycle block on boot saying which provider it bound, whether it found an existing schema or applied a fresh one, and whether demo seeding ran. That one block separates a stale volume from a code problem in about five seconds, and it is the first thing worth pasting into an issue. A second labelled block reports integration readiness. For the operator-side view of the same logs, see Operations and Troubleshooting.

Clone this wiki locally