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). diff --git a/docs/adr-index.md b/docs/adr-index.md new file mode 100644 index 0000000..5a09f1d --- /dev/null +++ b/docs/adr-index.md @@ -0,0 +1,47 @@ +# ADR index (§70) + +| ADR | Decision | +|---|---| +| ADR-001 | Modular monolith | +| ADR-002 | Node.js 24 LTS runtime | +| ADR-003 | TypeScript 6.0.3 pending TS7.1 ecosystem review | +| ADR-004 | Fastify 5 HTTP runtime | +| ADR-005 | PostgreSQL 18 sole canonical DB | +| ADR-006 | Kysely contained inside DB adapter | +| ADR-007 | React SSR for public rendering | +| ADR-008 | React/Vite admin | +| ADR-009 | Zero-JS public baseline | +| ADR-010 | Client-island model for optional public interactivity | +| ADR-011 | JSON Schema + TypeBox + Ajv validation | +| ADR-012 | EPPP Extension API hides framework internals | +| ADR-013 | Node/Amber is a theme extension | +| ADR-014 | Blog is a first-party content extension | +| ADR-015 | Versioned block documents | +| ADR-016 | Versioned page composition | +| ADR-017 | Extension-owned migrations/tables | +| ADR-018 | Docker Compose primary installation | +| ADR-019 | Opaque DB-backed admin sessions | +| ADR-020 | Separate anonymous preference identity | +| ADR-021 | Local media storage through storage port | +| ADR-022 | PostgreSQL jobs before external broker | +| ADR-023 | No Redis initially | +| ADR-024 | No microservices initially | +| ADR-025 | Trusted build-time executable extensions in v1 | +| ADR-026 | Exact dependency pinning + controlled upgrade lanes | +| ADR-027 (v1.1) | `core.markdown` block restores Markdown authoring inside the block model | +| ADR-028 (v1.1) | Hardened outbound fetch as a core service; extensions never fetch directly | +| ADR-029 (v1.1) | Embed provider allowlist enforced in renderer and generated CSP | +| ADR-030 (v1.1) | Native server-side SVG charts and diagrams with an accessibility contract | +| ADR-031 (v1.1) | Theme renaming replaces trademark references | +| ADR-032 (v1.1) | Day-one byte budgets; latency targets from measurement | + +Every ADR contains: Context, Decision, Alternatives, Consequences, Operational impact, Revisit trigger (§46 E01-S01). + +## Architectural litmus tests (§71) + +- **Add Ledger/Paper:** create `theme-paper` extension → register manifest/tokens/assets → optionally override renderer slots → tests → include in build. Failure = editing Home/Post domain, core DB, auth, or `if (theme === "paper")` in core. +- **Add Reading:** create `org.eppp.reading` → migrations → public route → admin contribution → Home section → settings → job(s) → content/block contributions. Failure = core learning seam/half-life/bookmark/link-health semantics. + +## Architecture review gates (§68) + +Gate A (end Sprint 1): publish a real post without core becoming blog/Amber-specific. Gate B (end Sprint 3): add a Home feature as an extension with no core edits. Gate C (end Sprint 4): a radically different theme runs without changing content/business logic. Gate D (end Sprint 5): visitor preference persists without coupling to auth or theme storage. Gate E (before public SDK): Extension API v1 proven enough to maintain. Gate F (Reading): Reading owns its whole domain without `if (readingEnabled)` in core. 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. diff --git a/docs/board-and-workflow.md b/docs/board-and-workflow.md new file mode 100644 index 0000000..064a4e2 --- /dev/null +++ b/docs/board-and-workflow.md @@ -0,0 +1,39 @@ +# Board & workflow + +## How the work is tracked + +Every initiative, epic, story and card is a Gitea **issue**. Each issue carries: + +- one `kind/*` label: `kind/initiative` · `kind/epic` · `kind/story` · `kind/task` +- one workflow `status/*` label (the four board columns) +- (epics/stories/tasks) a **sprint milestone**: `Sprint 0` … `Sprint 7`, `Sprint M` + +Parents link to children by stable ID in the body; children link to their parent by `#issue-number`. + +## Column mapping (status labels) + +| Column | Label | +|---|---| +| Backlog | `status/proposed` | +| To Do | `status/ready` | +| In Progress | `status/in-progress` | +| Done | `status/done` | + +Initial assignment: initiatives/epics → `status/proposed`; **Sprint 0** stories/tasks → `status/ready` (To Do); everything else → `status/proposed` (Backlog). Nothing starts In Progress or Done. + +## Milestones + +`Sprint 0`, `Sprint 1`, `Sprint 2`, `Sprint 3`, `Sprint 4`, `Sprint M`, `Sprint 5`, `Sprint 6`, `Sprint 7`. Sequencing (Sprint M after Sprint 4) is documented here and in `sprints-and-roadmap.md`; milestone objects carry no due dates because the sprint cadence (OD-01/OD-02) is a *proposed default awaiting sign-off*. + +## Creating the Gitea Projects board (manual) + +Gitea's Projects (Kanban) feature has **no REST API** in 1.23.7, so it cannot be created programmatically. To create it in the web UI (labels already agree with the columns): + +1. In `Fabrika/PersonalBlog`, open **Projects** → **New Project**. +2. Name it `PersonalBlog Delivery` (template: None/Basic Kanban). +3. Create columns in order: **Backlog**, **To Do**, **In Progress**, **Done**. +4. Add issues as cards. Because every issue already carries the matching `status/*` label, you can also filter the issue list by label and drag issues onto the matching column — the board and the labels stay in sync. + +## Numbering reconciliation + +v1.1 reused `E26`–`E28` (Sprint M), colliding with v1.0 Sprint 7 `E26`. Resolution: v1.0 IDs `E00`–`E26` unchanged; Sprint M epics renumbered `E26→E29` (media pipeline), `E27→E30` (embeds), `E28→E31` (charts/diagrams). Full detail in `programme.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 `