docs: add EPPP architecture & programme documentation #366
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
@@ -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 `<nav>`, editable labels/order/visibility, external URL validation, disabled-extension destination yields an admin warning (never a silently broken link).
|
||||||
|
|
||||||
|
## Column vs JSONB (§23.3)
|
||||||
|
|
||||||
|
Normal columns for data routinely constrained/joined/sorted/indexed/referenced/filtered. JSONB for versioned block props, settings, page section configuration, and sparse namespaced metadata.
|
||||||
|
|
||||||
|
## Core table namespace (§23.1)
|
||||||
|
|
||||||
|
`eppp_sites`, `eppp_site_settings`, `eppp_users`, `eppp_admin_sessions`, `eppp_content`, `eppp_content_revisions`, `eppp_page_compositions`, `eppp_navigation`, `eppp_extensions`, `eppp_extension_settings`, `eppp_extension_migrations`, `eppp_visitor_identities`, `eppp_visitor_preferences`, `eppp_media`, `eppp_jobs`, `eppp_job_runs`, `eppp_core_migrations`. Extension tables use the `ext_<name>_<table>` prefix.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Engineering standards
|
||||||
|
|
||||||
|
## TypeScript (§10)
|
||||||
|
|
||||||
|
Server baseline: `target ES2023`, `module NodeNext`, `moduleResolution NodeNext`, `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitOverride`, `useUnknownInCatchVariables`, `verbatimModuleSyntax`, `declaration`, `sourceMap`. Admin/Vite may use `moduleResolution: "Bundler"`. ESM-first; no `any` in public SDK contracts; runtime data validated even when TS types exist; public contract types narrow and intentionally versioned.
|
||||||
|
|
||||||
|
## Data access & transactions (§24)
|
||||||
|
|
||||||
|
Kysely exists only in the DB adapter/extension-storage implementation: `PostListSection → ContentQueryService → ContentRepository (interface) → PostgresContentRepository → Kysely/pg`. Mutating commands are transactional (validate → BEGIN create revision / update entry / update publish state → COMMIT → best-effort domain event).
|
||||||
|
|
||||||
|
## Migrations (§25)
|
||||||
|
|
||||||
|
Core migration IDs immutable and ordered. Each extension owns an independent chain in `eppp_extension_migrations`. Startup: connect → advisory migration lock → core migrations → discover/validate manifests → extension migrations → release lock → activate registries → readiness. Never edit a released migration; fix mistakes in a new one; destructive changes require backup/recovery notes; favour expand/contract; failure identifies extension/migration ID; replicas cannot race; PG major upgrades are separate operator procedures.
|
||||||
|
|
||||||
|
## Test strategy (§58)
|
||||||
|
|
||||||
|
- **Unit (Vitest 4.1.10):** content transitions, slug rules, manifest/version validation, theme/preference resolution, settings validation, block schema/migration, navigation validation.
|
||||||
|
- **Integration (real PostgreSQL 18.6):** repositories, transactions, migrations, advisory lock, sessions, preference persistence, extension migration ledger, queries/indexes. Fastify injection for route-level integration.
|
||||||
|
- **E2E (Playwright 1.62.1):** bootstrap/login, create draft, draft-not-public, publish, edit/revision, Home composition, navigation, logout/session expiry, theme selection, preference persistence, extension disable safety. Public flows run in Chromium, Firefox, WebKit.
|
||||||
|
- **Container smoke:** build → compose → ready → bootstrap fixture → publish → fetch Home/article → restart → verify persistence.
|
||||||
|
|
||||||
|
## CI/CD quality gates (§59)
|
||||||
|
|
||||||
|
Every PR: frozen install, formatting/lint, typecheck, unit tests, architecture fitness tests, PostgreSQL integration tests, admin build, server/public build, manifest/schema validation. Main/release additionally: Docker image build, Compose smoke, Playwright critical E2E, migrate-from-previous fixture, restore test, dependency/security scan, SBOM generation.
|
||||||
|
|
||||||
|
## Architecture fitness tests (§57, CI-enforced)
|
||||||
|
|
||||||
|
FIT-001 core imports no concrete extension · FIT-002 no Amber token values in core · FIT-003 unique namespaced extension IDs · FIT-004 unique contribution IDs · FIT-005 manifests validate · FIT-006 default settings validate · FIT-007 old document fixtures render/migrate · FIT-008 disable/re-enable without data loss · FIT-009 invalid/missing theme falls back · FIT-010 browser bundles can't import server/DB packages · FIT-011 UI can't import Kysely/pg · FIT-012 no core hydration bundle · FIT-013 migration IDs immutable/unique · FIT-014 golden tuple passes · FIT-015 no outbound HTTP outside hardened fetch · FIT-016 chart SVG has no literal colour values · FIT-017 embed iframes only allowlisted + CSP matches.
|
||||||
|
|
||||||
|
## Definition of Ready (§44)
|
||||||
|
|
||||||
|
A story enters a sprint only when: outcome, acceptance criteria, owning module, core-vs-extension ownership, data/schema impact, migration requirement, security impact, failure states, UX states, API impact, observability need, test strategy, dependencies are all known. "Add plugin support" is not a ready story.
|
||||||
|
|
||||||
|
## Definition of Done (§45)
|
||||||
|
|
||||||
|
Acceptance criteria pass; dependency rules pass; runtime input validated; unit/integration/E2E tests pass; immutable migrations; failure state implemented/tested; accessibility + responsive verified; security addressed; diagnostics appropriate; schema/API version bumped if needed; older fixtures still work/migrate; docs updated; Docker build succeeds; Compose smoke passes; peer review completed.
|
||||||
|
|
||||||
|
## Non-functional requirements (§66)
|
||||||
|
|
||||||
|
Latency targets are measurement-first (reference hardware agreed in Sprint 7). **Byte budgets are day-one and CI-asserted (v1.1):** public HTML <30 KB compressed; CSS <20 KB compressed; public JS outside opt-in islands = 0 bytes (FIT-012); each island <10 KB compressed; third-party requests on initial load = zero.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Extension architecture
|
||||||
|
|
||||||
|
Internal Fastify plugins (organise server internals) are a deliberately different concept from **EPPP extensions** (the public product contract). An EPPP extension never receives unrestricted Fastify/Kysely access.
|
||||||
|
|
||||||
|
## Manifest v1 (§16.2)
|
||||||
|
|
||||||
|
`ExtensionManifestV1`: id, name, version, `extensionApiVersion: "1"`, minimumCoreVersion, type (`theme`|`feature`|`content`|`integration`), dependencies, capabilities, settingsSchema, contributes (routes/contentTypes/blocks/sections/themes/jobs/events/admin).
|
||||||
|
|
||||||
|
IDs must be namespaced (`org.eppp.reading`, `org.eppp.theme-amber`, `com.example.gallery`); unqualified IDs (`reading`, `theme1`) are rejected.
|
||||||
|
|
||||||
|
## Extension context v1 (§16.3)
|
||||||
|
|
||||||
|
`ExtensionContextV1` exposes only narrow EPPP-owned services: `extension`, `settings`, `routes`, `contentTypes`, `blocks`, `sections`, `themes`, `jobs`, `events`, `storage`, `logger`. Raw Fastify instance, Kysely instance, pg pool, and application internals are never exposed through the public SDK.
|
||||||
|
|
||||||
|
## Lifecycle states (§16.4)
|
||||||
|
|
||||||
|
`installed → disabled / enabled / error / incompatible`. Disabled extensions retain data/settings but deactivate contributions. Incompatible or failed optional extensions provide diagnostics without taking unrelated public features down.
|
||||||
|
|
||||||
|
## Trust model (§16.5)
|
||||||
|
|
||||||
|
Extensions are **trusted deploy-time code**: package exists in build config → installed during image build → EPPP discovers/validates → admin enables/configures. No v1 workflow uploads and executes arbitrary npm packages. A future marketplace is a separate security architecture (provenance, signatures, scanning, capability enforcement, isolation, rollback).
|
||||||
|
|
||||||
|
## Activation sequence (§17)
|
||||||
|
|
||||||
|
```
|
||||||
|
manifest valid? → API version supported? → core version satisfies range?
|
||||||
|
→ dependencies present? → migrations successful? → settings validate?
|
||||||
|
→ activate contributions
|
||||||
|
```
|
||||||
|
|
||||||
|
Failures map to `incompatible` / `error` / `config-error` states; diagnostics visible in admin and structured logs.
|
||||||
|
|
||||||
|
## Extension package layout (§61)
|
||||||
|
|
||||||
|
```
|
||||||
|
extensions/reading/
|
||||||
|
package.json · eppp.manifest.json
|
||||||
|
server/{index.ts, migrations/, jobs/, repositories/}
|
||||||
|
admin/index.tsx · public/islands/ · styles/ · tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Only documented entry points are importable; extensions never import arbitrary EPPP internal files.
|
||||||
|
|
||||||
|
## Engineering conventions (§62)
|
||||||
|
|
||||||
|
No raw Fastify/Kysely/pg in the SDK; no generic `everythingContext`; no direct extension-to-extension internal import; real needs drive hooks (avoid speculative hook explosion); server/database internals never enter browser bundles; stored content references stable semantic IDs; framework upgrades hidden behind EPPP contracts; architecture rules automated in CI when mechanically enforceable.
|
||||||
|
|
||||||
|
## Validation (§22)
|
||||||
|
|
||||||
|
JSON Schema is canonical for HTTP input, selected HTTP output, extension manifests/settings, block props, page section settings, import/export documents, and selected event payloads. TypeBox authors typed schemas; Ajv validates runtime data. **TypeScript types are not runtime validation.**
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Programme
|
||||||
|
|
||||||
|
## Hierarchy (§43)
|
||||||
|
|
||||||
|
```
|
||||||
|
Initiative → Epic → Capability → Story → Engineering task/subtask
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initiatives
|
||||||
|
|
||||||
|
| ID | Initiative | Outcome |
|
||||||
|
|---|---|---|
|
||||||
|
| I-01 | Publishing Core | Owner can publish a real site |
|
||||||
|
| I-02 | Extensibility Platform | Themes/features add cleanly |
|
||||||
|
| I-03 | Administration | Meaningful site behaviour is editable |
|
||||||
|
| I-04 | Visitor Personalisation | Owner-controlled visitor preferences |
|
||||||
|
| I-05 | Operational Simplicity | Install/upgrade/backup are predictable |
|
||||||
|
| I-06 | Extension Ecosystem | Reading, RSS, themes and later capabilities |
|
||||||
|
|
||||||
|
## Initiative → epic map (editorial primary mapping)
|
||||||
|
|
||||||
|
- **I-01** — E02, E03, E04, E06, E07, E08, E09, E10, E29, E30, E31
|
||||||
|
- **I-02** — E11, E12, E13, E14, E15, E16
|
||||||
|
- **I-03** — E05, E19
|
||||||
|
- **I-04** — E17, E18
|
||||||
|
- **I-05** — E00, E01, E20, E21, E22, E23, E24, E25, E26
|
||||||
|
- **I-06** — (future v0.2/v0.3 scope; no v1 epics)
|
||||||
|
|
||||||
|
## Numbering scheme
|
||||||
|
|
||||||
|
```
|
||||||
|
Initiative I-01 … I-06
|
||||||
|
Epic E00 … E31
|
||||||
|
Story E<epic>-S<story> e.g. E00-S01
|
||||||
|
Task/Card E<epic>-S<story>-T<nn> e.g. E00-S01-T01
|
||||||
|
Epic-level task (no stories) E<epic>-T<nn> (only E26)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Numbering reconciliation (v1.1)
|
||||||
|
|
||||||
|
v1.0 uses epic IDs `E00`–`E26`. The v1.1 amendment (§52-A) reused `E26`–`E28` for the new Sprint M epics, colliding with Sprint 7's `E26`. Because v1.1 is additive ("adds, does not delete"), v1.0 IDs stay unchanged and Sprint M epics receive fresh IDs:
|
||||||
|
|
||||||
|
- Sprint M "media pipeline" (§52-A `E26`) → **E29**
|
||||||
|
- Sprint M "embeds" (§52-A `E27`) → **E30**
|
||||||
|
- Sprint M "native charts and diagrams" (§52-A `E28`) → **E31**
|
||||||
|
|
||||||
|
Their stories renumber accordingly (e.g. `E26-S01` → `E29-S01`). `E27`/`E28` are retired.
|
||||||
|
|
||||||
|
## Requirements treated as confirmed (§2.1)
|
||||||
|
|
||||||
|
Home + individual posts; Amber ("Node") initial appearance; everything meaningful editable; plugin/extension-oriented; themes as extensions; owner may later permit visitor theme choice; visitor choice remembered in browser/session; deployment extremely simple; Docker Compose preferred; delivery in epics/stories/sprints/acceptance criteria; fast demo without irreversible shortcuts. **v1.1 restored:** Markdown authoring (13.1); rich media (29.1–29.4); external graph/diagram embeds (29-A); page metadata (34-A).
|
||||||
|
|
||||||
|
## Proposed defaults requiring sign-off (§2.2)
|
||||||
|
|
||||||
|
OD-01 two-week sprints after Sprint 0 · OD-02 Sprint 0 = one week · OD-03 one site per installation · OD-04 one administrator · OD-05 admin idle timeout 12h / absolute 7d · OD-06 anonymous preference retention 365d · OD-07 local filesystem media storage · OD-08 `/posts/:slug` canonical route · OD-09 soft delete · OD-10 build-time trusted extensions · OD-11 Vite modern-browser baseline · OD-12 amd64+arm64 · OD-13 (v1.1) Markdown via `core.markdown` · OD-14 (v1.1) seed embed allowlist. OD-03/04/08/09 need explicit product sign-off before semantics freeze.
|
||||||
|
|
||||||
|
## Definition of Ready / Done
|
||||||
|
|
||||||
|
See [engineering-standards.md](engineering-standards.md) (§44/§45).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Rich content & media
|
||||||
|
|
||||||
|
Images and video are first-class; embeds/charts/diagrams are a founding requirement (v1.1 §29-A restores what v1.0 omitted).
|
||||||
|
|
||||||
|
## Media pipeline (§29.1–29.4, v1.1)
|
||||||
|
|
||||||
|
- **Storage port** `ObjectStorage` (put/get/stat/delete); initial adapter stores on `/var/lib/eppp/media`; future adapters target S3/R2/MinIO without changing content records (content stores media IDs, never absolute paths).
|
||||||
|
- **Upload validation (§29.1):** size checked before reading the body; content type from magic bytes (client header/extension advisory only); SHA-256 on ingest (duplicate checksum returns the existing record); generated content-addressed keys, never the original filename.
|
||||||
|
- **Image processing (§29.2):** every accepted image re-encoded before serving (original bytes never served); metadata stripped, orientation baked in then dropped (GPS/camera serial never reach the public volume); derivative widths 480/800/1200/1600/2400 (never upscaled), AVIF + WebP per width + one universal JPEG fallback; rendered images carry `width`/`height` (zero CLS); lazy-load below the fold; immutable one-year cache headers.
|
||||||
|
- **Video posture (§29.3):** no transcoding in v1; accept MP4 (H.264 + AAC) only, reject others with a message naming export settings; probe dimensions/duration, generate a poster frame.
|
||||||
|
|
||||||
|
## Embeds — three tiers (§29-A.1)
|
||||||
|
|
||||||
|
| Tier | Mechanism | Survives provider death | Public JS |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 Native | chart/diagram blocks rendered server-side to SVG | yes | none |
|
||||||
|
| 2 Snapshot | embed + locally cached static image | degrades to image | none |
|
||||||
|
| 3 Live | sandboxed iframe from allowlisted host, click-to-load | no | one island |
|
||||||
|
|
||||||
|
The editor steers to tier 1 whenever data belongs to the author ("paste data" beside "paste embed URL").
|
||||||
|
|
||||||
|
## Provider allowlist (§29-A.2)
|
||||||
|
|
||||||
|
`eppp_embed_providers`: exact hostnames only (no wildcards), admin-editable, every change audited. Seed: Observable, Datawrapper, CodePen, `youtube-nocookie.com`, `player.vimeo.com`, `archive.org`, plus a configurable self-hosted Grafana host. Enforced twice: in the renderer and in a CSP `frame-src` generated from the same table.
|
||||||
|
|
||||||
|
## Fetch, cache & privacy (§29-A.3)
|
||||||
|
|
||||||
|
On save of content containing an embed: resolve provider by exact host (unknown host = author-visible error); fetch oEmbed through the hardened fetch; sanitise to an iframe with allowlisted `src` + dimensions; download and re-host the thumbnail (never hotlink); refresh on a 30-day cycle. Live iframes are click-to-load; every iframe carries `sandbox`, `referrerpolicy="no-referrer"`, `loading="lazy"`, a required `title`. **No reader request reaches a third party before explicit interaction.**
|
||||||
|
|
||||||
|
## Hardened outbound fetch (§29-A.4, core)
|
||||||
|
|
||||||
|
One shared core service (oEmbed, thumbnail download, future link checker): resolve DNS first and reject private/loopback/link-local/multicast/metadata ranges; re-check after every redirect (max 3), pinning the connection to the pre-resolved address (closes DNS-rebinding); 10 s timeout, 5 MB cap; worker egress additionally restricted at the network layer. No extension ever writes its own outbound fetch (FIT-015).
|
||||||
|
|
||||||
|
## Native charts & diagrams (§29-A.5)
|
||||||
|
|
||||||
|
`core.chart` (line, bar, area, scatter; CSV/JSON props) and `core.diagram` (Mermaid), rendered server-side to a single inline `<svg>`: every chart carries `<title>`, `<desc>` and a visually-hidden data table; all colour comes from theme tokens (a literal colour in chart output is a build failure, FIT-016); renderers are pure functions (no headless browser, no client bundle, no network); limits are configuration (2000 points / 12 series default, exceeded at save time).
|
||||||
|
|
||||||
|
Ownership: blocks/UX ship as first-party extensions (`org.eppp.embeds`, `org.eppp.charts`, or one `org.eppp.rich-content`); hardened fetch + CSP generation are core (FIT-015–017).
|
||||||
|
|
||||||
|
## Metadata & syndication (§34-A, v1.1)
|
||||||
|
|
||||||
|
Core (every content type needs it): per-page `<title>`, meta description, canonical URL, Open Graph tags, JSON-LD `BlogPosting` (headline, datePublished, dateModified, author) on articles; `sitemap.xml` regenerated on publish; `robots.txt` excluding `/admin` and `/api`; `ETag`/`Last-Modified` with conditional requests. RSS remains a v0.2 extension. Open Graph images (when added) are generated server-side (SVG rasterised at publish, cached as a media object), no headless browser.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Security & operations
|
||||||
|
|
||||||
|
## Security baseline (§32)
|
||||||
|
|
||||||
|
Minimum controls: Argon2id passwords; secure HttpOnly cookies; CSRF mitigation; CSP/security headers; login + abuse-endpoint rate limiting; parameterised SQL; JSON Schema input validation; output escaping; safe URL validation; upload limits; no raw plugin HTML by default; extension manifest validation; no arbitrary runtime package installation; secrets outside source control; least-privilege PostgreSQL role where possible.
|
||||||
|
|
||||||
|
v1.1 additions: SSRF-hardened outbound fetch (§29-A.4); image re-encoding + metadata stripping (§29.2); embed provider allowlist enforced in renderer + generated CSP `frame-src` (§29-A.2); click-to-load live embeds (§29-A.3). Raw HTML is not an initial block; any future raw-HTML capability requires a dedicated privileged threat model.
|
||||||
|
|
||||||
|
## Threat boundaries (§63)
|
||||||
|
|
||||||
|
**Trusted:** core code, reviewed built-in/build-time extension code, operator deployment config.
|
||||||
|
**Untrusted:** HTTP input, admin form input, post content, uploaded files, imported bundles, cookies, extension setting values, external URLs, link-check responses, future webhooks. Every untrusted boundary validates before domain use.
|
||||||
|
|
||||||
|
## Authentication & sessions (§26)
|
||||||
|
|
||||||
|
Local administrator; Argon2id hash; opaque DB-backed session (no JWT for browser admin). Browser holds a random opaque token; DB stores only a one-way lookup hash. Cookie `eppp_admin`: Secure (prod), HttpOnly, SameSite=Lax, Path=/. Server sessions enable logout/revocation/forced expiry/password-change invalidation.
|
||||||
|
|
||||||
|
## Visitor preferences (§27)
|
||||||
|
|
||||||
|
Separate from admin auth. Cookie `eppp_pref` (opaque token; only a lookup hash at rest). Identity created lazily only when a preference changes (≥256-bit randomness); no tracking record per anonymous page view. Generic keys: `appearance.theme`, `appearance.density`, `reading.grouping`, `locale.language`.
|
||||||
|
|
||||||
|
## Error model & observability (§31)
|
||||||
|
|
||||||
|
Canonical errors: ValidationError, AuthenticationError, AuthorizationError, NotFoundError, ConflictError, ExtensionError, ExtensionCompatibilityError, ExtensionMigrationError, ThemeResolutionError, StorageError, DatabaseError. Admin API error shape: `{ error: { code, message, requestId } }`. Never expose stack traces, SQL, tokens/cookies, filesystem internals, or secrets. Pino logs carry timestamp, level, request_id, operation, duration_ms, site_id, extension_id, content_id, error_code; redact passwords/hashes/cookies/tokens/db secrets.
|
||||||
|
|
||||||
|
## Docker Compose (§35–§38)
|
||||||
|
|
||||||
|
Reference operator flow: `cp .env.example .env` → set secrets → `docker compose up -d` → browse `http://localhost:8080`. No host Node/pnpm/PostgreSQL/Vite/Argon2 toolchains required. `compose.yml` runs `db` (postgres, health-gated) + `app` (non-root, read-only root fs, tmpfs `/tmp`); optional `worker` via `--profile worker`; optional Caddy edge profile (`compose.edge.yml`) for TLS. Required secrets: `POSTGRES_PASSWORD`, `EPPP_SESSION_SECRET` (production docs must include a copy/paste secret-generation command).
|
||||||
|
|
||||||
|
## Health/readiness (§40)
|
||||||
|
|
||||||
|
`/health/live` answers only whether the process can respond (not gated on PostgreSQL). `/health/ready` verifies PostgreSQL reachable, core migrations current, registry graph valid, required extensions activated. A broken optional extension is marked failed/disabled rather than making unrelated routes unready.
|
||||||
|
|
||||||
|
## Backup, restore, export/import (§41, v1.1)
|
||||||
|
|
||||||
|
A complete backup: PostgreSQL + media volume + deployment config (secrets regenerated) + release identifier/image digest + extension IDs/versions. Logical `pg_dump`/restore is the portable baseline; restore drill runs monthly as an automated scheduled job against the latest real backup (v1.1). Backups are encrypted client-side and stored off-host; reference retention 48 hourly / 30 daily / 12 weekly / 12 monthly (v1.1).
|
||||||
|
|
||||||
|
Portable export contains versioned manifest, site config, content, chosen revisions, page compositions, navigation, extension config, selected media. Import validates format version, core compatibility, extension dependencies, block versions, settings schemas, duplicate IDs/slugs — never silently partially import invalid data.
|
||||||
|
|
||||||
|
## Operational failure philosophy (§65)
|
||||||
|
|
||||||
|
Prefer "degrade optional capability + clear diagnostic" over "entire site unavailable": missing optional theme → fallback; broken Reading → Reading route unavailable, blog remains; corrupt preference → ignore/default; PostgreSQL unavailable → not ready; invalid core migration → not ready.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Sprints & roadmap
|
||||||
|
|
||||||
|
## Sprint map (sequenced; Sprint M slots after Sprint 4 per v1.1 §52-A)
|
||||||
|
|
||||||
|
| Sprint | Goal | Epics |
|
||||||
|
|---|---|---|
|
||||||
|
| Sprint 0 | Architecture runway | E00, E01 |
|
||||||
|
| Sprint 1 | First end-to-end publishing slice | E02, E03, E04, E05, E06, E07 |
|
||||||
|
| Sprint 2 | Editable Home and site structure | E08, E09, E10 |
|
||||||
|
| Sprint 3 | Extension runtime hardening | E11, E12, E13, E14 |
|
||||||
|
| Sprint 4 | Theme platform proof | E15, E16 |
|
||||||
|
| Sprint M | Media and rich content (v1.1) | E29, E30, E31 |
|
||||||
|
| Sprint 5 | Visitor personalisation (deferred) | E17, E18 |
|
||||||
|
| Sprint 6 | Installation, backup and upgrades | E19, E20, E21, E22 |
|
||||||
|
| Sprint 7 | V1 hardening | E23, E24, E25, E26 |
|
||||||
|
|
||||||
|
Story count is exactly **96**: Sprint 0 (6) · Sprint 1 (17) · Sprint 2 (12) · Sprint 3 (13) · Sprint 4 (6) · Sprint M (11) · Sprint 5 (7) · Sprint 6 (13) · Sprint 7 (11).
|
||||||
|
|
||||||
|
## Sprint goals
|
||||||
|
|
||||||
|
- **Sprint 0:** a new engineer can clone, build, test and run EPPP; architectural boundaries are executable.
|
||||||
|
- **Sprint 1:** an authenticated owner can create/publish a simple article and an anonymous visitor can read it in Amber.
|
||||||
|
- **Sprint 2:** the owner controls visible identity, Home composition and navigation without source edits.
|
||||||
|
- **Sprint 3:** a new non-core capability can be added without feature-specific core changes.
|
||||||
|
- **Sprint 4:** prove Amber is one implementation of a stable theme contract.
|
||||||
|
- **Sprint M:** an article with image/video/embed/native chart publishes with zero core hydration and no third-party request before interaction.
|
||||||
|
- **Sprint 5:** (deferred until a second production theme exists) owner may expose multiple themes; anonymous visitors persist an explicit choice without becoming tracked identities.
|
||||||
|
- **Sprint 6:** EPPP is easy to recover and upgrade, not merely easy to demo.
|
||||||
|
- **Sprint 7:** security, accessibility, failure handling and performance are trusted for long-lived deployment and extension development.
|
||||||
|
|
||||||
|
## Resequenced implementation order (§72-A)
|
||||||
|
|
||||||
|
1–23 as v1.0 (workspace → Docker → PostgreSQL adapter → config → migration runner → Fastify shell → manifest loader → registries → site model → content/revisions → Blog → block registry → theme registry → Amber → React server renderer → auth → admin shell → post editor → Home composition → navigation → generic settings → extension lifecycle → second test theme). Then: 24 media pipeline (E29) → 25 hardened fetch/embeds (E30) → 26 charts/diagrams (E31) → 27 metadata/sitemap (34-A) → 28 operational hardening → 29 visitor preferences (deferred) → 30 Reading.
|
||||||
|
|
||||||
|
## Roadmap summary (§73)
|
||||||
|
|
||||||
|
```
|
||||||
|
Sprint 0 Architecture + Docker + DB + CI
|
||||||
|
Sprint 1 Publish a real post in Amber
|
||||||
|
Sprint 2 Editable Home/site/navigation
|
||||||
|
Sprint 3 Extension API/lifecycle proof
|
||||||
|
Sprint 4 Theme platform + second test theme
|
||||||
|
Sprint M Media + embeds + charts (v1.1)
|
||||||
|
Sprint 5 Visitor preferences/theme choice
|
||||||
|
Sprint 6 Setup/backup/export/upgrade
|
||||||
|
Sprint 7 Security/accessibility/performance hardening
|
||||||
|
v0.2 Second production theme + RSS + lightweight features
|
||||||
|
v0.3 Reading extension as full architecture proof
|
||||||
|
```
|
||||||
|
|
||||||
|
## v0.1 release boundary (§54)
|
||||||
|
|
||||||
|
**Included:** Docker Compose, PostgreSQL 18, admin auth, site identity editing, Home, posts, draft/publish, revision foundation, Amber theme, content-type/block/theme registries, extension manifest/registry baseline, page section registry, editable Home, editable navigation, responsive/a11y baseline, backup docs, health/readiness.
|
||||||
|
|
||||||
|
**Excluded:** Reading, link-health, RSS, public theme selector (while only Amber ships), plugin marketplace, arbitrary runtime package upload, multi-site SaaS, multi-admin RBAC, collaborative editing, Redis, Elasticsearch, Kubernetes requirement, microservices.
|
||||||
|
|
||||||
|
## v0.2 / v0.3 (§55/§56)
|
||||||
|
|
||||||
|
v0.2 (personalisation + lightweight extensions): second production theme (Ledger/Paper), visitor theme choice + preference persistence, RSS extension, Running/Now Home extension, static page content type, media improvements. v0.3: Reading (`org.eppp.reading`) owning bookmarks, imports, seam/half-life taxonomy, search/filter/grouping, link health, background link checker, archive fallback, admin, public route, Home contribution, settings, migrations — installable, configurable, disableable, upgradeable through public contracts.
|
||||||
|
|
||||||
|
## Decisions deliberately deferred (§69)
|
||||||
|
|
||||||
|
rich-text editor; drag/reorder UI library; CI vendor; dependency update bot; SBOM/security scanner; metrics exporter; object-store provider; multi-admin RBAC; multi-site tenancy; external search engine; public extension marketplace.
|
||||||
|
|
||||||
|
## Risk register (§67, summary)
|
||||||
|
|
||||||
|
Fastify v5 EOL unknown (M) · TS7 transition (M) · Kysely pre-1.0 (M) · arbitrary plugin code owns process (Critical if allowed) · theme overrides fragment semantics (M) · extension migrations corrupt data (H) · preference ID becomes tracking ID (M) · page builder scope explosion (H) · React public JS bloat (M) · dual DB complexity (M/H) · background infra proliferation (M) · Extension API freezes too early (M) · (v1.1) programme outlives builder attention before motivating features ship (H) · (v1.1) embed surface unspecified despite founding requirement (H) · (v1.1) dependency pins go stale (M).
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Theme architecture
|
||||||
|
|
||||||
|
Amber (the default) is a normal theme extension and must use the same contract future themes use.
|
||||||
|
|
||||||
|
## Theme contract v1 (§18)
|
||||||
|
|
||||||
|
`ThemeDefinitionV1`: id, name, version, tokens (background, panel, foreground, foregroundBright, foregroundMuted, foregroundFaint, border, borderStrong, accent, positive, warning, negative, fontUi, fontBody, radius, contentMeasure), settingsSchema, assets, renderers (partial slots).
|
||||||
|
|
||||||
|
## Controlled renderer slots (§18.1)
|
||||||
|
|
||||||
|
Token-first themes are preferred; more structural themes override only defined slots: `site.shell`, `site.header`, `site.footer`, `page.home`, `page.article`, `content.post-list`, `ui.navigation`, `ui.metadata`. Themes never replace routing, authentication or persistence.
|
||||||
|
|
||||||
|
## CSS layers (§18.2)
|
||||||
|
|
||||||
|
```css
|
||||||
|
@layer reset; @layer core; @layer components; @layer extension; @layer theme; @layer site-overrides;
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables use the EPPP namespace.
|
||||||
|
|
||||||
|
## Fallback (§18.3)
|
||||||
|
|
||||||
|
visitor preference → (valid + enabled + owner-permitted?) → site default → (valid + enabled?) → emergency built-in core fallback. A theme error must never cause a blank site.
|
||||||
|
|
||||||
|
## Theme naming (§18.4, v1.1)
|
||||||
|
|
||||||
|
Two v1.0 names referenced trademarks and one collided with the runtime. Approved shipping names (visual direction unchanged):
|
||||||
|
|
||||||
|
| Shipping | v1.0 name | Character |
|
||||||
|
|---|---|---|
|
||||||
|
| `amber` | Node | default; amber phosphor on near-black, status-page home |
|
||||||
|
| `bevel` | X11 | beveled window chrome, grey desktop, green phosphor |
|
||||||
|
| `warden` | Shodan | dark custodial green and red |
|
||||||
|
| `lattice` | GITS | cool cyan network topology |
|
||||||
|
| `paper` | Ledger | light, print-correct, high-contrast |
|
||||||
|
|
||||||
|
`amber` names the phosphor, not the runtime (`org.eppp.theme-amber`). "Shodan" and "GITS" must not appear in code, package IDs or the public repository. `paper` is the accessible high-contrast light option that prints correctly.
|
||||||
|
|
||||||
|
## Theme proof (Sprint 4)
|
||||||
|
|
||||||
|
Complete `ThemeRegistry` (registerTheme, listThemes, getTheme, getSiteDefaultTheme, resolveTheme), Amber extraction audit (tokens only in `theme-amber`, core cannot import it), restrained theme settings (accent override, content width, font scale), a component gallery, a deliberately different **Test Light theme** (not shipped publicly) to expose leaked assumptions, and failure scenarios (removed/disabled theme, invalid manifest, missing asset) — every public request falls back safely.
|
||||||
Reference in New Issue
Block a user