From 37c508ca7c8a1011a1017d1d41cb181174858807 Mon Sep 17 00:00:00 2001 From: kpcto Date: Thu, 27 Aug 2026 08:14:18 +0000 Subject: [PATCH 01/13] Add EPPP architecture/programme docs index --- docs/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b0254bb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# EPPP — PersonalBlog: Architecture & Programme documentation + +This directory holds the architecture and programme documentation for **EPPP** (Extensible Personal Publishing Platform) as built in `Fabrika/PersonalBlog`. + +> **Why here and not the Gitea wiki:** the Gitea wiki REST API on this instance (Gitea 1.23.7) is broken — its read path hardcodes branch `master` while wiki writes go to `main` (the instance default). Until the instance is patched or `DEFAULT_BRANCH` is set to `master`, the wiki feature cannot be created/read programmatically. These pages are the same content, kept in-repo so they are versioned and reviewable. + +**Source of truth:** `EPPP/docs/EPPP_Architecture_v1.1_Product_Programme.md` (Architecture v1.1, 2026-08-27). + +## Contents + +- [Architecture](architecture.md) — executive decision, modular monolith, dependency direction, core ownership +- [Technology stack](technology-stack.md) — runtime stack, golden tuple, container pins +- [Domain model](domain-model.md) — Site/ContentEntry/ContentRevision, block document, `core.markdown`, page composition +- [Extension architecture](extension-architecture.md) — manifest/context, lifecycle, trust model +- [Theme architecture](theme-architecture.md) — theme contract, tokens, renderer slots, naming +- [Rich content & media](rich-content-and-media.md) — media pipeline, embeds, charts/diagrams, metadata +- [Security & operations](security-and-operations.md) — security baseline, auth, Docker/Compose, backup/restore +- [Engineering standards](engineering-standards.md) — TS config, validation, test strategy, CI/CD +- [ADR index](adr-index.md) — ADR-001 … ADR-032 +- [Programme](programme.md) — initiatives, hierarchy, numbering, Definition of Ready/Done +- [Sprints & roadmap](sprints-and-roadmap.md) — sprint map, roadmap, v0.2/v0.3, deferred decisions +- [Board & workflow](board-and-workflow.md) — workflow labels, milestones, board setup, numbering reconciliation + +## Work tracking + +Every initiative (`I-*`), epic (`E*`), story (`E*-S*`) and card (`E*-S*-T*`) is a **Gitea issue** in this repository, labelled with a `kind/*` label and a workflow `status/*` label, and assigned to a sprint milestone. See [board-and-workflow.md](board-and-workflow.md). -- 2.54.0 From ad577fe7b680ad31b51b4a066dc59f08435fa1e2 Mon Sep 17 00:00:00 2001 From: kpcto Date: Thu, 27 Aug 2026 08:16:11 +0000 Subject: [PATCH 02/13] Add architecture docs --- docs/architecture.md | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a9d7d79 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,95 @@ +# Architecture + +## Executive decision (§1) + +EPPP is implemented as a **TypeScript modular monolith**: Node.js 24 LTS, Fastify 5, PostgreSQL 18 (sole canonical DB), React 19 (server-rendered public components + Vite admin), and Docker Compose as the primary runtime model. + +The first visible release is intentionally small: editable site identity, editable Home page, ordered list of published posts, individual post pages, administration interface, and the **Amber** theme. + +**Central product rule:** EPPP v0.1 is small because optional features are absent, not because extensibility is absent. + +From the first release the codebase has explicit versioned boundaries for: content types, content blocks, page sections, themes, extensions, extension settings, navigation, visitor preferences, database migrations, media storage, background jobs, events, and rendering. + +EPPP is **not** a microservice system, a Next.js app, a WordPress clone, or a browser-installable arbitrary-code marketplace. + +## Architecture style (§4) + +**Modular monolith.** One application image contains the public server, admin API, rendering pipeline, extension runtime, core services, built admin assets and first-party extensions. An optional worker uses the same image with a different command (`eppp serve` / `eppp worker`). + +``` + Internet + | + Optional edge proxy (Caddy 2.11.4) + | + +---------------------------+ + | EPPP APP — Node 24 / | + | Fastify 5 | + | Public React SSR | Admin | + | React + Vite | + +------------+---------------+ + | + Application Core + Sites | Content | Revisions | Pages | Navigation | Media + Blocks | Themes | Extensions | Settings | Preferences + Auth | Jobs | Events | Rendering + | + Registry layer + ContentType | Block | Section | Theme | Extension | Job + | + PostgreSQL 18.6 +``` + +**Why not microservices:** no v1 requirement justifies distributed transactions, service discovery, brokers, multi-service deployment, or cross-service schema changes. Module boundaries allow future extraction on evidence. + +## Repository & dependency architecture (§9) + +``` +eppp/ + apps/server · apps/admin + packages/contracts · core · extension-sdk · database-postgres · rendering-react · ui-core · config · testing + extensions/core-blog · theme-amber + migrations/ · tooling/ · docs/{adr, extension-development, operations} + Dockerfile · compose.yml · compose.edge.yml · .env.example · pnpm-workspace.yaml · pnpm-lock.yaml · package.json · tsconfig.base.json +``` + +### Dependency direction (§9.1) + +Allowed: +- `apps/server -> core/application -> contracts` +- `extensions/* -> extension-sdk -> contracts` +- `database-postgres -> core ports -> Kysely/pg` + +Forbidden: +- `core -> theme-amber`, `core -> core-blog`, `core -> reading` +- `theme-amber -> database internals` +- extension A → extension B internals +- public browser code → server-only package + +CI enforces these via import/dependency architecture tests (FIT-001, FIT-010, FIT-011). + +## Core ownership (§11) + +**Core owns:** application lifecycle; site identity/configuration; authentication; content persistence primitives; revisions; page composition; navigation; extension discovery/activation/settings; theme resolution; content-type/block/section/theme registries; visitor preferences; media storage port; job registry; event bus; migration coordinator; public rendering pipeline; admin API infrastructure; health/readiness; error/observability conventions; hardened outbound fetch (v1.1, §29-A.4); page metadata rendering (v1.1, §34-A). + +**Core does not own** (extensions own these): Reading/bookmarks, bookmark seams/half-life, RSS, Amber-specific CSS, projects/running-list, comments, analytics, newsletter integrations, link-health. + +## Public rendering (§19) + +Public pages are server-rendered first. Pipeline: Fastify route → SiteResolver → VisitorPreferenceResolver → ContentService → PageComposition + BlockRegistry → ThemeResolver → React DOM server renderer → HTML. + +**Zero-JavaScript baseline (§19.1):** Home, article, navigation, metadata and footer work with JS disabled. React is a server-rendering detail; the site is not hydrated. Core public Home/article target: **0 bytes** EPPP JavaScript. + +**Client islands (§19.2):** only truly interactive features register islands (theme selector, Reading filters, graph, comments, gallery), each with an activation mode (`load`/`idle`/`visible`/`interaction`). + +## Administration (§20) + +React client app built with Vite, served by the same image. `/admin/*` → `/api/admin/v1/*`. Initial IA: Dashboard, Content→Posts, Home, Navigation, Appearance (Active theme, Theme settings), Extensions, Settings→Site. + +## v0.1 public surface (§42) + +``` +GET / · GET /posts/:slug · GET /assets/* · GET /media/* +GET /health/live · GET /health/ready · GET /admin/* +``` + +Admin API lives under `/api/admin/v1/*`. Initial Home composition: `core.site-intro` + `core.post-list`; post list defaults to published only, newest first, deterministic ordering, configurable limit. -- 2.54.0 From 57f810093c92de3df2c4a064b44e9352ad3ecdde Mon Sep 17 00:00:00 2001 From: kpcto Date: Thu, 27 Aug 2026 08:16:12 +0000 Subject: [PATCH 03/13] Add technology stack docs --- docs/technology-stack.md | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/technology-stack.md diff --git a/docs/technology-stack.md b/docs/technology-stack.md new file mode 100644 index 0000000..8ccf55f --- /dev/null +++ b/docs/technology-stack.md @@ -0,0 +1,58 @@ +# Technology stack + +Researched 2026-08-26. Three support classes (§5.1): **A** = fixed upstream EOL date; **B** = documented support policy, no fixed multi-year date; **C** = rolling, exact-pinned + tested + upgraded deliberately. + +## Runtime stack (§5.2) + +| Layer | Version | Class | +|---|---|---| +| Node.js | 24.19.0 LTS (Krypton) | A | +| TypeScript | 6.0.3 | C | +| Fastify | 5.12.1 | B | +| PostgreSQL | 18.6 | A | +| `pg` | 8.22.0 | C | +| Kysely | 0.29.4 | C | +| React / React DOM | 19.2.8 | C | +| `@sinclair/typebox` | 0.34.52 | project-designated LTS | +| Ajv | 8.20.0 | C | +| Pino | 10.3.1 | B | +| `argon2` | 0.45.1 | C | +| `@fastify/cookie` / `helmet` / `rate-limit` / `static` / `csrf-protection` / `swagger` | 11.1.2 / 13.1.1 / 11.2.0 / 10.1.3 / 8.0.1 / 9.8.1 | Fastify lifecycle | +| `@fastify/multipart` | 10.1.1 | C/ecosystem (CI gate before enabling) | + +## v1.1 verification note (§5.2.1) + +An independent re-check on 2026-08-27 found: **TypeScript verified correct** (7.0 GA'd 2026-07-08 without a stable programmatic API before 7.1; 6.0 is the bridge). **PostgreSQL "18.6" could not be confirmed** — postgresql.org shows 18.3 (2026-02-26) with quarterly minors, placing late August at 18.4/18.5. Sprint 0 re-runs the provenance table (§74) and commits the re-verified tuple. A "verified" pin is trusted for one sprint, not the life of the document. + +## Build/admin/test toolchain (§5.3) + +Vite 8.2.2 · `@vitejs/plugin-react` 6.1.0 · pnpm 11.23.0 · Vitest 4.1.10 · Playwright 1.62.1 · Docker Engine 29.7.2 (CI ref) · Docker Compose 5.5.0 (CI ref) · Caddy 2.11.4 (optional). + +## Golden compatibility tuple (§7) + +Every release candidate must pass the full integration suite on: Node 24.19.0, TypeScript 6.0.3, Fastify 5.12.1, PostgreSQL 18.6, pg 8.22.0, Kysely 0.29.4, React/React DOM 19.2.8, TypeBox 0.34.52, Ajv 8.20.0, Pino 10.3.1. Certified container platforms: `linux/amd64`, `linux/arm64`. Playwright runs Chromium, Firefox, WebKit. + +## Container image pins (§5.4) + +``` +node:24.19.0-bookworm-slim # application (glibc → argon2 native deps) +postgres:18.6-bookworm +caddy:2.11.4-alpine # optional edge profile +``` + +Release automation records immutable image digests in an SBOM/release manifest. + +## Version discipline (§8) + +Direct platform dependencies are exact-pinned in release branches; the lockfile is committed; no caret ranges for core runtime/build deps. `packageManager: pnpm@11.23.0`, `engines.node: 24.x`. + +Update lanes: **Security emergency** (immediate, focused + full CI) · **Patch** (weekly batch) · **Minor** (monthly review) · **Major** (explicit programme item: ADR + migration/compatibility). + +## LTS strategy (§6) + +- **Node 24** EOL 2028-04-30; evaluate Node 26 only after it is LTS + compatibility CI passes; major change requires ADR. +- **PostgreSQL 18** supported to 2030-11-14; always run current minor; major upgrade is a separate operator procedure. +- **Fastify 5** — no fixed EOL claimed; upgrade to v6 gated on: stable v6, all plugins compatible, full suite green, extension contracts stable/migrated, ADR. +- **React** — exact-pinned 19.2.8, no RSC in v1, kept out of persisted content formats. +- **TypeScript** — 6.0.3 baseline, formal review after TS 7.1 stable. +- **Kysely** — pre-1.0, contained inside the PostgreSQL adapter; domain/extension packages never import it. -- 2.54.0 From 1ecebac76a9a468f79120ed7b709717337cfd8ad Mon Sep 17 00:00:00 2001 From: kpcto Date: Thu, 27 Aug 2026 08:16:40 +0000 Subject: [PATCH 04/13] Add domain model docs --- docs/domain-model.md | 80 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/domain-model.md diff --git a/docs/domain-model.md b/docs/domain-model.md new file mode 100644 index 0000000..d3415c1 --- /dev/null +++ b/docs/domain-model.md @@ -0,0 +1,80 @@ +# Domain model + +## Site (§12.1) + +``` +Site: id UUID, name TEXT, description TEXT NULL, canonical_url TEXT, + default_theme_id TEXT, created_at/updated_at TIMESTAMPTZ +``` + +Even though v1 proposes one site per installation, `site_id` scopes persisted data, leaving a migration path to multi-site without designing tenancy now. + +## Content entry (§12.2) + +``` +ContentEntry: id UUID, site_id UUID, content_type TEXT, slug TEXT, title TEXT, + status TEXT, excerpt TEXT NULL, published_at TIMESTAMPTZ NULL, + current_revision_id UUID NULL, created_at/updated_at, deleted_at NULL +``` + +Initial statuses: `draft`, `published`. Initial registered content type: `core.post`. Future content types are registry contributions, not `if type === ...` branches. + +## Content revision (§12.3) + +``` +ContentRevision: id UUID, content_id UUID, revision_number BIGINT, + document_version INTEGER, document JSONB, created_by UUID, created_at +``` + +Unique `(content_id, revision_number)`. + +## Identifier policy (§12.4) + +PostgreSQL 18 provides UUIDv7 generation — use UUIDv7 for major aggregates (globally unique opaque IDs with better temporal/index locality than UUIDv4). + +## Canonical content document (§13) + +A post body is a versioned semantic block document: + +```json +{ "version": 1, "blocks": [ { "id": "019d...", "type": "core.paragraph", "version": 1, "props": { "text": "Hello world." } } ] } +``` + +Every block persists block instance ID, globally unique block type ID, block schema version, and validated properties. Theme CSS classes / concrete theme markup must not be stored as semantic content. + +## `core.markdown` block (§13.1, v1.1) + +Markdown authoring was a locked product decision omitted from v1.0; it is restored as a block inside the block model. `props.source` is CommonMark + GFM (tables, strikethrough, task lists, autolinks) + footnotes; raw HTML disabled. Rules: + +1. Exactly **one** server-side Markdown renderer ending in one sanitiser with an explicit allowlist; the admin preview calls it (no second client-side renderer). +2. A v0.1 post is a single `core.markdown` block by default — editor is a Markdown text editor with server-rendered preview. +3. When the block editor arrives, new content splits into finer blocks; existing `core.markdown` blocks are never bulk-converted. +4. Export emits `props.source` as `.md` files with frontmatter — the portability guarantee. + +## Initial blocks (§14) + +`core.heading`, `core.paragraph`, `core.quote`, `core.code`, `core.divider`, `core.image` (once media ships). **v1.1:** `core.markdown` joins the set as the primary authoring block. + +Missing block behaviour (§14.1): public page still renders with a safe placeholder (no raw props, error logs include IDs); admin shows the missing dependency and preserves the original payload (no silent deletion). + +## Page composition (§15) + +Home is an ordered list of versioned section instances: + +```json +{ "version": 1, "sections": [ { "id": "home-intro", "type": "core.site-intro", "version": 1, "enabled": true, "settings": {} }, { "id": "home-posts", "type": "core.post-list", "version": 1, "enabled": true, "settings": { "limit": 20 } } ] } +``` + +Initial composition UI supports only enable / disable / reorder / configure. Explicitly excluded: arbitrary pixel positioning, nested no-code canvas, user-authored raw HTML layouts, raw CSS editor. + +## Navigation (§28) + +Persisted model: id, site_id, label, destination_type, destination, position, enabled, open_in_new_context, created_at, updated_at. Initial destination types: `internal_route`, `external_url`. Semantic `