Files
PersonalBlog/docs/development/non-container.md
T
implementer f3cd70e45d
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 46s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m15s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 44s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m44s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Failing after 56s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Skipped
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Skipped
docs: document the lint command and the CI quality baseline stages (E00-S05-T01)
The non-container guide now covers pnpm lint (the formatting/lint policy) in
the Test and clean-clone smoke sections and describes the ordered PR CI
stages locked in by tests/ci-stages.test.mjs.
2026-08-30 06:12:25 +00:00

11 KiB

Local non-container development

This guide documents the local, non-container developer path for the EPPP workspace: how to install, build and run the project on your machine without Docker. The containerised path (Docker Compose baseline, container smoke tests) is tracked separately as story [E00-S02] Docker Compose baseline and is not covered here.

Status: this document tracks the E00-S01 workspace bootstrap state. Commands and expected outputs below were verified from a clean clone.

What you get at bootstrap

The workspace is a pnpm monorepo with three package groups:

Group Path Purpose
apps/ apps/server (@personal-blog/server) Public server application. Serves the application health endpoint (E00-S02-T03) gated on the startup migration run (E00-S03-T06), fails fast at startup with a field-specific error when a required setting is missing (E00-S04-T02), redacts secret values from all log output (E00-S04-T03), and reads all of its settings through the config package's environment adapter — no process.env reads in the server (E00-S04-T04); the Fastify 5 application shell lands in a later story.
packages/ packages/core (@personal-blog/core) Application core (site identity, content primitives). Bootstrap placeholder.
packages/ packages/config (@personal-blog/config) Configuration service. Owns the TypeBox/Ajv configuration schema for the validated config fields (E00-S04-T01), the field-specific startup error for a missing required setting (E00-S04-T02), the secret redaction layer (E00-S04-T03) and the environment adapter — the workspace's single owner of process.env reads, mapping HOST/PORT/DATABASE_URL/EPPP_SESSION_SECRET onto the validated config (E00-S04-T04) and validating HOST as a hostname or IP address at the adapter boundary; the committed .env.example template (E00-S04-T05) documents every variable with placeholder values only.
packages/ packages/database-postgres (@personal-blog/database-postgres) PostgreSQL database adapter package. Single owner of the pg/Kysely driver imports (E00-S03-T02); the migration ledger (schema_migrations, E00-S03-T03), the migration advisory lock (E00-S03-T04) and the migration runner with its failure diagnostic (E00-S03-T05) are implemented here. The server depends on this package to run the startup migrations behind its readiness gate (E00-S03-T06).
extensions/ extensions/example (@personal-blog/example-extension) Example extension exercising the extensions/ group. Bootstrap placeholder.

A dependency-boundary rule (dependency-boundaries.json, enforced by tests/architecture-import.test.mjs) keeps the groups from collapsing into an accidental dependency graph: no packages/* package may import a concrete extensions/* package.

Prerequisites

  • Git — to clone the repository.
  • Node.js 24.x — required. The root package.json restricts the Node engine to 24.x (engines.node), and the committed pnpm-workspace.yaml sets engineStrict: true, so pnpm install on any other Node version is rejected (ERR_PNPM_UNSUPPORTED_ENGINE) instead of merely warned. Install Node 24.x via nvm, fnm or another version manager to match CI.
  • pnpm 11.23.0 — pinned via the packageManager field in the root package.json. The easiest way to get exactly this version is Corepack, which ships with Node.js (corepack enable).

No database, no runtime services and no Docker daemon are required at this stage.

Install

# 1. Clone the repository
git clone https://git.stevanovic.co.uk/Fabrika/PersonalBlog.git
cd PersonalBlog

# 2. Activate the pinned pnpm (11.23.0) via Corepack
corepack enable

# 3. Install dependencies against the committed lockfile (reproducible)
pnpm install --frozen-lockfile
  • The install is frozen: --frozen-lockfile refuses to run if pnpm-lock.yaml is out of date with the manifests, so every clone gets the exact same dependency tree. CI uses the same flag.
  • If you are adding or changing dependencies, run pnpm install (without the flag) and commit the resulting pnpm-lock.yaml so the frozen install keeps working for everyone.
  • node_modules/ and the pnpm content-addressable store are git-ignored and never committed.

Build

# Compile every workspace package (apps, packages, extensions) into dist/
pnpm build

# Typecheck every workspace package (no emit)
pnpm typecheck

pnpm build runs tsc -p tsconfig.json in each package in topological order and emits dist/ (JavaScript + type declarations + source maps) per package. Expected result: apps/server/dist/, packages/core/dist/, packages/config/dist/, packages/database-postgres/dist/ and extensions/example/dist/ are produced, all packages report Done, exit 0. dist/ is git-ignored; rebuild whenever you change src/.

Run

# Run the compiled public server entrypoint
EPPP_SESSION_SECRET='change-me-0123456789abcdefghijklmnopqrstuvwxyz' pnpm --filter @personal-blog/server start

This runs the start script of apps/server (node dist/index.js), i.e. the compiled application entrypoint. Three things to know:

  1. The command must follow pnpm build — the start script executes the compiled artifact in dist/, it does not compile first.
  2. Since [E00-S04-T02] the server validates its required settings at startup: the admin-session secret EPPP_SESSION_SECRET (the config schema's required field, ≥ 32 characters, Security-and-Operations §32/§26) must be set in the environment — if it is missing, the process fails fast with a field-specific startup error (missing required setting: sessionSecret) that names the missing field instead of booting. Provide it in your shell or a local .env file copied from the committed .env.example template (E00-S04-T05).
  3. Since [E00-S02-T03], apps/server serves the application health endpoint: starting it opens an HTTP server on port 3000 answering GET /health, so the process stays up. Since [E00-S03-T06] the endpoint is the readiness probe: when a DATABASE_URL is configured, the app runs the startup migrations before reporting ready — GET /health answers HTTP 503 with {"status":"not ready"} while the run is in flight and HTTP 200 with {"status":"ok"} only after it completes. With no DATABASE_URL (this local non-container path) there is no migration run to wait for, so the server reports ready immediately and GET /health answers 200 {"status":"ok"} from the start. The real Fastify 5 application shell — which turns this into the full serving API — lands in a later story; the start command shape stays the same once it does.
  4. Since [E00-S04-T03], the server logs its resolved configuration at startup with secret values redacted: the first log line is [config] resolved configuration: {"host":"0.0.0.0","port":3000, ..., "sessionSecret":"[REDACTED]"}, and every log line passes through the redacting logger — the admin-session secret and the password embedded in a DATABASE_URL connection string never appear in the log output.
  5. Since [E00-S04-T04], all settings flow through the config package's environment adapter (loadConfigFromEnv in @personal-blog/config — the workspace's single owner of process.env reads): HOST, PORT, DATABASE_URL and EPPP_SESSION_SECRET are mapped onto the validated config shape (defaults: host 0.0.0.0, port 3000, no databaseUrl) and validated at startup — no module outside the config package reads process.env directly. HOST is validated at the adapter boundary as a hostname or IP address (an invalid value fails startup with a field-specific error naming host instead of being logged) and the server passes config.host to server.listen, so a configured HOST binds exactly that interface — e.g. HOST=127.0.0.1 binds loopback only — and the startup log line (@personal-blog/server listening on http://<host>:<port>) reflects the actual bind.

To run the compiled output of any other workspace package directly:

node <package-dir>/dist/index.js   # e.g. node packages/core/dist/index.js

Test

pnpm test
pnpm lint

pnpm test runs the node:test suites under tests/ with zero extra dependencies (this is also the suite that enforces the dependency-boundary rule). pnpm lint runs the formatting/lint policy suite (tests/formatting-policy.test.mjs): every tracked text file must use LF line endings, no BOM, no trailing whitespace, no tab indentation and exactly one final newline; JSON files must additionally parse, carry no duplicate keys and use 2-space indentation.

Pull requests run these checks as CI stages, in order — frozen lockfile install → typecheck → formatting/lint → unit → architecture → PostgreSQL integration → build of the applications (E00-S05-T01); the stage order, the needs chain and the tests/ coverage are locked in by tests/ci-stages.test.mjs.

Smoke check from a clean clone

git clone https://git.stevanovic.co.uk/Fabrika/PersonalBlog.git && cd PersonalBlog
corepack enable
pnpm install --frozen-lockfile   # exit 0, lockfile untouched
pnpm build                       # 5/5 packages emit dist/, exit 0
pnpm typecheck                   # 5/5 packages pass --noEmit, exit 0
pnpm lint                        # formatting/lint policy passes, exit 0
pnpm test                        # all node:test suites pass, exit 0
pnpm --filter @personal-blog/server start   # requires EPPP_SESSION_SECRET (see [Run](#run)); serves GET /health on port 3000, stays up

Troubleshooting

Symptom Cause / fix
pnpm: command not found Corepack shims not activated — run corepack enable, or prefix commands with corepack pnpm ....
ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is out of date with the manifests. Run pnpm install (unfrozen) and commit the lockfile update.
ERR_PNPM_UNSUPPORTED_ENGINE on install Your Node version is outside the supported 24.x engine line (engines.node in the root package.json, enforced by engineStrict: true in pnpm-workspace.yaml). Install Node 24.x (e.g. via nvm, fnm or another version manager).
start exits immediately with no output The server crashed or exited at startup — check the process output. Since [E00-S02-T03] the entrypoint serves GET /health on port 3000 and stays up; a missing pnpm build (stale/absent dist/) is the usual cause (see Run).
start fails with missing required setting: sessionSecret Since [E00-S04-T02] the server validates its required settings at startup: the admin-session secret EPPP_SESSION_SECRET (≥ 32 chars) is missing or too short — set it in your shell or a local .env file (see Run).
.env files .env/.env.* are git-ignored; the committed .env.example template (E00-S04-T05) shows placeholder values only.

Out of scope

  • Root build/test/typecheck script wiring — covered by E00-S01-T05; only usage is documented here.
  • Docker Compose baseline and container smoke tests — E00-S02.
  • PostgreSQL adapter, migration runner and other runtime services — later E00 stories.