CI / Frozen lockfile install (push) Successful in 42s
CI / Secrets not embedded (E00-S02-T08) (push) Successful in 28s
CI / Database-postgres import isolation (E00-S03-T02) (push) Successful in 24s
CI / Migration ledger (E00-S03-T03) (push) Successful in 42s
CI / Migration advisory lock (E00-S03-T04) (push) Successful in 48s
CI / Migration failure diagnostic (E00-S03-T05) (push) Successful in 46s
CI / App readiness after migrations (E00-S03-T06) (push) Successful in 1m4s
CI / Field-specific startup errors (E00-S04-T02) (push) Successful in 1m5s
CI / Secret redaction from logs (E00-S04-T03) (push) Successful in 1m6s
CI / Env adapter owns process.env (E00-S04-T04) (push) Successful in 1m12s
CI / Compose config (E00-S03-T01) (push) Successful in 26s
CI / TypeBox/Ajv config schema (E00-S04-T01) (push) Successful in 1m4s
CI / .env.example placeholders only (E00-S04-T05) (push) Successful in 29s
Co-authored-by: bot-implementer <bot-implementer@fabrika.internal>
182 lines
10 KiB
Markdown
182 lines
10 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
node <package-dir>/dist/index.js # e.g. node packages/core/dist/index.js
|
|
```
|
|
|
|
## Test
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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](#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](#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.
|