Files
PersonalBlog/docs/development/non-container.md
T
implementer 25248461e0
CI / Frozen lockfile install (pull_request) Successful in 50s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 29s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 33s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 44s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 46s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Successful in 44s
CI / App readiness after migrations (E00-S03-T06) (pull_request) Successful in 1m12s
CI / Field-specific startup errors (E00-S04-T02) (pull_request) Successful in 1m12s
CI / Secret redaction from logs (E00-S04-T03) (pull_request) Successful in 1m15s
CI / TypeBox/Ajv config schema (E00-S04-T01) (pull_request) Successful in 53s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
docs: document the redacted resolved-configuration log in the non-container guide (E00-S04-T03)
2026-08-30 03:52:55 +00:00

9.2 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), and redacts secret values from all log output (E00-S04-T03); 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) and the secret redaction layer (E00-S04-T03); the environment adapter (E00-S04-T04) and the .env.example template (E00-S04-T05) land in later tasks.
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 (the .env.example template lands in E00-S04).
  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.

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 test runs the node:test suites under tests/ (currently tests/architecture-import.test.mjs, 10 tests) with zero extra dependencies. This is also the suite that enforces the dependency-boundary rule.

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 test                        # 10/10 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; a committed .env.example template lands with the environment story (E00-S04).

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.