From 06294119662d9ae9c5266c707b4f9cd28171921a Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 4 Jul 2026 14:36:16 +0200 Subject: [PATCH] Add architecture documentation, ADRs, and operations concept Initial deliverable of the architecture phase: 16 ADRs (stack, CRDT collaboration, plugin sandbox, import/export, backups, CI/CD), data model, permission model, real-time collaboration and plugin concepts, deployment/operations/security documentation, and the milestone roadmap that the implementation issues are derived from. Co-Authored-By: Claude Fable 5 --- LICENSE | 21 ++ README.md | 53 ++++++ docs/architecture/README.md | 105 ++++++++++ .../adr/0001-typescript-monorepo.md | 55 ++++++ docs/architecture/adr/0002-postgresql.md | 48 +++++ .../adr/0003-yjs-crdt-collaboration.md | 64 +++++++ docs/architecture/adr/0004-tiptap-editor.md | 57 ++++++ .../adr/0005-react-vite-frontend.md | 51 +++++ .../adr/0006-nestjs-prisma-backend.md | 54 ++++++ .../adr/0007-auth-sessions-oidc-ready.md | 61 ++++++ docs/architecture/adr/0008-plugin-sandbox.md | 81 ++++++++ .../adr/0009-import-export-converters.md | 57 ++++++ .../adr/0010-postgres-fulltext-search.md | 49 +++++ .../adr/0011-file-storage-quotas.md | 53 ++++++ docs/architecture/adr/0012-i18n.md | 46 +++++ .../adr/0013-versioning-and-trash.md | 68 +++++++ .../adr/0014-gitea-actions-cicd.md | 56 ++++++ docs/architecture/adr/0015-backup-strategy.md | 57 ++++++ .../adr/0016-self-hosted-fonts.md | 47 +++++ docs/architecture/data-model.md | 180 ++++++++++++++++++ docs/architecture/deployment.md | 101 ++++++++++ docs/architecture/operations.md | 86 +++++++++ docs/architecture/permissions.md | 107 +++++++++++ docs/architecture/plugin-architecture.md | 109 +++++++++++ docs/architecture/realtime-collaboration.md | 96 ++++++++++ docs/architecture/roadmap.md | 107 +++++++++++ docs/architecture/security.md | 100 ++++++++++ 27 files changed, 1969 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/adr/0001-typescript-monorepo.md create mode 100644 docs/architecture/adr/0002-postgresql.md create mode 100644 docs/architecture/adr/0003-yjs-crdt-collaboration.md create mode 100644 docs/architecture/adr/0004-tiptap-editor.md create mode 100644 docs/architecture/adr/0005-react-vite-frontend.md create mode 100644 docs/architecture/adr/0006-nestjs-prisma-backend.md create mode 100644 docs/architecture/adr/0007-auth-sessions-oidc-ready.md create mode 100644 docs/architecture/adr/0008-plugin-sandbox.md create mode 100644 docs/architecture/adr/0009-import-export-converters.md create mode 100644 docs/architecture/adr/0010-postgres-fulltext-search.md create mode 100644 docs/architecture/adr/0011-file-storage-quotas.md create mode 100644 docs/architecture/adr/0012-i18n.md create mode 100644 docs/architecture/adr/0013-versioning-and-trash.md create mode 100644 docs/architecture/adr/0014-gitea-actions-cicd.md create mode 100644 docs/architecture/adr/0015-backup-strategy.md create mode 100644 docs/architecture/adr/0016-self-hosted-fonts.md create mode 100644 docs/architecture/data-model.md create mode 100644 docs/architecture/deployment.md create mode 100644 docs/architecture/operations.md create mode 100644 docs/architecture/permissions.md create mode 100644 docs/architecture/plugin-architecture.md create mode 100644 docs/architecture/realtime-collaboration.md create mode 100644 docs/architecture/roadmap.md create mode 100644 docs/architecture/security.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9a22d8e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Stefan Waidele and Dorfteich contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38dabe7 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Dorfteich + +Dorfteich is an open-source wiki system built around **ponds** (German: +*Teiche*) — self-contained wiki spaces that people and teams organize freely +with hierarchical labels, directories, and Obsidian-style page relations. +Pages are edited in a collaborative WYSIWYG editor with live cursors and +offline support. + +- Project site: +- Public flagship instance: +- License: [MIT](LICENSE) + +## Key features + +- **Real-time collaboration** — multiple people edit the same page + simultaneously; everyone sees the other participants' cursors and input + live. Offline edits merge conflict-free on reconnect (CRDT-based). +- **Ponds** — isolated wiki spaces with their own members, permissions, + fonts, and page organization. Every registered person gets a personal pond. +- **Flexible organization** — hierarchical labels, free page ordering, + `[[wikilinks]]` with backlinks. A classic page tree is possible but never + enforced. +- **Fine-grained permissions** — roles (Site Admin, Pond Admin, Editor, + Reader, Public) can be granted per pond, per label, or per page; the most + specific setting wins. +- **Import & export** — Markdown as the primary exchange format, plus + best-effort structural import from Word/OpenOffice and export to + Word/OpenOffice/PDF. +- **Plugins** — sandboxed extensions (custom blocks, styles, page tools) + installable at runtime without redeploying the instance. +- **Self-hosting first** — a single `docker compose up` plus a guided + first-run setup wizard yields a working instance. + +## Repository layout + +| Path | Contents | +| --- | --- | +| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations | +| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds | +| `packages/` | Shared packages (types, permission logic, plugin SDK) | +| `deploy/` | Docker Compose stacks and deployment tooling | + +## Status + +The project is in the architecture and backlog phase. Implementation stories +are tracked as issues in this repository. Start reading at +[`docs/architecture/README.md`](docs/architecture/README.md). + +## Contributing + +Code, comments, and documentation are written in English. Write clear code +that humans can follow easily; when in doubt, prefer readability over +cleverness. All contributions are accepted under the MIT license. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..5708804 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,105 @@ +# Dorfteich — Architecture Overview + +This directory is the authoritative architecture documentation. Every +implementation story references the documents here; when a story and this +documentation disagree, clarify before coding. + +## System context + +Dorfteich is shipped as a set of Docker containers behind a reverse proxy. +One deployment = one **instance** (e.g. `dorfteich.online`, or a self-hosted +installation). + +```mermaid +flowchart LR + subgraph clients [Clients] + B[Browser SPA
React + TipTap + Yjs] + end + subgraph instance [Dorfteich instance - Docker Compose] + RP[Reverse proxy] + WEB[web
static SPA assets] + API[api
NestJS REST] + COLLAB[collab
Hocuspocus WebSocket] + PG[(PostgreSQL)] + PAN[pandoc-server
doc conversion] + GOT[Gotenberg
HTML to PDF] + VOL[/uploads + plugins volume/] + end + B -- HTTPS --> RP + RP --> WEB + RP -- /api --> API + RP -- /collab WebSocket --> COLLAB + API --> PG + COLLAB --> PG + API --> PAN + API --> GOT + API --> VOL + COLLAB -. permission checks .-> API +``` + +- **web** serves the single-page application (static assets). +- **api** owns all business logic: auth, ponds, pages, labels, permissions, + quotas, import/export, plugin management, admin functions. +- **collab** synchronizes CRDT documents (page content) and awareness + (cursors) over WebSocket and persists document state to PostgreSQL. It + authenticates clients with short-lived tokens issued by **api**. +- **pandoc-server** and **Gotenberg** are internal-only conversion sidecars + (Word/OpenOffice import/export, PDF export). +- All page content lives in PostgreSQL; binary uploads (images, attachments) + and installed plugins live on a Docker volume. + +## Documents + +### Architecture Decision Records (`adr/`) + +| ADR | Decision | +| --- | --- | +| [0001](adr/0001-typescript-monorepo.md) | TypeScript everywhere, pnpm monorepo | +| [0002](adr/0002-postgresql.md) | PostgreSQL as the only database | +| [0003](adr/0003-yjs-crdt-collaboration.md) | Yjs CRDT + Hocuspocus for real-time and offline collaboration | +| [0004](adr/0004-tiptap-editor.md) | TipTap (ProseMirror) as the WYSIWYG editor | +| [0005](adr/0005-react-vite-frontend.md) | React + Vite single-page application | +| [0006](adr/0006-nestjs-prisma-backend.md) | NestJS + Prisma for the API server | +| [0007](adr/0007-auth-sessions-oidc-ready.md) | Cookie sessions, Argon2id, OIDC-ready identity model | +| [0008](adr/0008-plugin-sandbox.md) | Sandboxed iframe plugins with a message-based API | +| [0009](adr/0009-import-export-converters.md) | Pandoc + Gotenberg sidecars for import/export | +| [0010](adr/0010-postgres-fulltext-search.md) | PostgreSQL full-text search behind a search interface | +| [0011](adr/0011-file-storage-quotas.md) | Filesystem volume for uploads, DB-tracked quotas | +| [0012](adr/0012-i18n.md) | i18next with German and English from the start | +| [0013](adr/0013-versioning-and-trash.md) | Page version history via Yjs snapshots, soft-delete trash | +| [0014](adr/0014-gitea-actions-cicd.md) | CI/CD with Gitea Actions, staged promotion | +| [0015](adr/0015-backup-strategy.md) | Nightly pg_dump + uploads sync, 30-day retention, off-host mirror | +| [0016](adr/0016-self-hosted-fonts.md) | Self-hosted Google Fonts, per-pond font configuration | + +### Concept documents + +| Document | Contents | +| --- | --- | +| [data-model.md](data-model.md) | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins | +| [permissions.md](permissions.md) | Role model and the "most specific setting wins" resolution algorithm | +| [realtime-collaboration.md](realtime-collaboration.md) | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks | +| [plugin-architecture.md](plugin-architecture.md) | Plugin manifest, packaging, sandbox runtime, extension points, admin flows | +| [deployment.md](deployment.md) | Compose stacks for Dev/Test/Int/Prod, environments, promotion pipeline | +| [operations.md](operations.md) | Monitoring, logging, backup/restore, update strategy for self-hosters | +| [security.md](security.md) | Threat-driven security concept: authn/authz, sandboxing, uploads, secrets | +| [roadmap.md](roadmap.md) | Epics and milestones; the order stories are implemented in | + +## Terminology + +| German (product vision) | English (code, docs, issues) | +| --- | --- | +| Teich | pond | +| Seite | page | +| Teich-Admin | Pond Admin | +| Bearbeitende | Editor | +| Lesende | Reader | +| Öffentlichkeit | Public | + +## Conventions for implementers + +- Language: English for code, comments, commit messages, and issues. +- Write clear, human-readable code; document the "why", not the "what". +- UI strings never appear hard-coded — always through i18n resources + (see ADR 0012), with German and English translations added in the same + change. +- Every story lists the ADRs it depends on; read them before starting. diff --git a/docs/architecture/adr/0001-typescript-monorepo.md b/docs/architecture/adr/0001-typescript-monorepo.md new file mode 100644 index 0000000..bdad6b7 --- /dev/null +++ b/docs/architecture/adr/0001-typescript-monorepo.md @@ -0,0 +1,55 @@ +# ADR 0001: TypeScript everywhere, pnpm monorepo + +- Status: accepted +- Date: 2026-07-04 + +## Context + +Dorfteich needs a browser-based collaborative editor (necessarily +JavaScript/TypeScript) and a server side that shares non-trivial logic with +the client: CRDT document handling, permission resolution, plugin manifest +validation, and shared type definitions. Stories will be implemented by many +independent contributors (including AI coding sessions) working in parallel; +consistency and low context-switching cost matter more than raw runtime +performance. + +## Decision + +- **One language: TypeScript** (strict mode) for frontend, API server, + collaboration server, and shared packages. Runtime: Node.js (current LTS). +- **One repository: a pnpm workspace monorepo** with this layout: + +``` +apps/web React SPA (ADR 0005) +apps/api NestJS REST API (ADR 0006) +apps/collab Hocuspocus collaboration server (ADR 0003) +apps/setup (part of api) first-run setup wizard endpoints +packages/shared shared types, permission resolution, validation schemas +packages/plugin-sdk plugin API typings + host/client messaging helpers +deploy/ Docker Compose stacks, Dockerfiles, backup scripts +docs/ this documentation +``` + +- Shared logic (e.g. the permission resolution algorithm, ADR/permissions.md) + lives in `packages/shared` and is imported by both `apps/api` and + `apps/web` — never duplicated. +- Tooling baseline: ESLint + Prettier (repo-wide config), Vitest for unit + tests, Playwright for end-to-end tests. + +## Consequences + +- Client and server cannot drift apart on types: API request/response schemas + are defined once (Zod schemas in `packages/shared`) and validated at + runtime on the server. +- The Yjs ecosystem (ADR 0003) is native to this stack; no bridging layer. +- Contributors need to know exactly one language and one package manager. +- CPU-heavy document conversion is deliberately **not** done in Node — + it is delegated to sidecar containers (ADR 0009). + +## Alternatives considered + +- **Go or Rust backend + TS frontend**: better raw performance, but splits + the CRDT logic across two languages (Yjs vs. yrs bindings), doubles the + skill surface, and buys nothing at the target scale (ADR 0010 context). +- **Multiple repositories**: rejected; cross-cutting stories (API + client) + would constantly need coordinated PRs across repos. diff --git a/docs/architecture/adr/0002-postgresql.md b/docs/architecture/adr/0002-postgresql.md new file mode 100644 index 0000000..f1b5259 --- /dev/null +++ b/docs/architecture/adr/0002-postgresql.md @@ -0,0 +1,48 @@ +# ADR 0002: PostgreSQL as the only database + +- Status: accepted +- Date: 2026-07-04 + +## Context + +Dorfteich stores relational data (users, ponds, pages, labels, grants, +quotas), binary CRDT document state, and needs full-text search. Self-hosting +must stay simple ("one `docker compose up`"), so every additional stateful +service raises the barrier. The capacity target is small-to-medium instances +(order of 100 ponds / 10,000 pages), decided in the project kickoff. + +## Decision + +- **PostgreSQL (current stable major) is the single database** for: + - all relational entities (see `data-model.md`), + - Yjs document state and incremental updates as `bytea` (ADR 0003), + - page version snapshots (ADR 0013), + - full-text search via `tsvector` (ADR 0010), + - job/outbox tables for e-mail sending and notifications (no separate + message broker). +- Schema migrations are managed with Prisma Migrate (ADR 0006) and run + automatically on API startup (`prisma migrate deploy`), so self-hosters + update by pulling new images (see `operations.md`). +- Binary user uploads (images, attachments) do **not** go into PostgreSQL — + they live on a filesystem volume (ADR 0011). Only metadata is stored in the + database. + +## Consequences + +- Exactly one stateful service to run, back up, and restore + (plus the uploads volume) — backup strategy stays a plain `pg_dump` + (ADR 0015). +- No Redis: rate limiting, session storage, and pub/sub needs are served by + PostgreSQL (sessions table, `LISTEN/NOTIFY` where needed). If horizontal + scaling of the collab server ever becomes necessary, introducing Redis + pub/sub is a contained change inside `apps/collab`. +- Full-text search quality is bounded by PostgreSQL FTS; ADR 0010 keeps the + door open for an external engine. + +## Alternatives considered + +- **SQLite**: attractive for tiny self-hosts, but concurrent-write behavior + under the collab server's persistence load and the FTS requirements make it + risky; supporting two databases doubles the test matrix. Rejected. +- **PostgreSQL + Redis + object storage from day one**: standard SaaS stack, + but oversized for the target scale and hostile to casual self-hosting. diff --git a/docs/architecture/adr/0003-yjs-crdt-collaboration.md b/docs/architecture/adr/0003-yjs-crdt-collaboration.md new file mode 100644 index 0000000..5a81094 --- /dev/null +++ b/docs/architecture/adr/0003-yjs-crdt-collaboration.md @@ -0,0 +1,64 @@ +# ADR 0003: Yjs CRDT + Hocuspocus for real-time and offline collaboration + +- Status: accepted +- Date: 2026-07-04 + +## Context + +The product vision requires: simultaneous editing with live cursor positions +of all participants, live-synced input, **and** (decided in kickoff) offline +editing with conflict-free merge on reconnect. Expected concurrency is small +groups per page. This rules out naive locking and makes Operational +Transformation (OT) unattractive: OT needs a central authority and handles +offline divergence poorly. CRDTs are the established answer for +offline-capable collaborative editing. + +## Decision + +- **Yjs** is the CRDT implementation. Page content is a Yjs document + (`Y.Doc`) containing a `Y.XmlFragment` bound to the editor (ADR 0004). +- **Hocuspocus** (the Yjs WebSocket server by the TipTap team, MIT) is the + collaboration server, running as its own container `apps/collab`: + - `onAuthenticate`: validates a short-lived collaboration token (JWT) + issued by the API; the token encodes user id, page id, and access level + (read-only vs. read-write). No token, no connection. + - `onLoadDocument` / `onStoreDocument`: loads and persists document state + to PostgreSQL (debounced writes; merged state plus an update log with + periodic compaction). + - Awareness protocol carries cursor positions, selections, and user + display info for the live-cursor UI. +- **Offline support** on the client: + - `y-indexeddb` persists every opened document locally; edits while + disconnected accumulate in IndexedDB and merge automatically on + reconnect (CRDT property — no conflict dialogs). + - The SPA is installable/cachable as a PWA (service worker caches the app + shell) so the editor loads without a network connection. + - Read-only permission is enforced server-side: the collab server rejects + updates on read-only connections; offline edits by users whose write + permission was revoked are rejected at sync time and the client informs + the user. +- **Cursor display**: TipTap's collaboration-cursor extension renders remote + cursors/selections from awareness states. +- Version history hooks into this layer via Yjs snapshots (ADR 0013). + +## Consequences + +- Conflict-free merging is guaranteed by construction; no merge UI needed. +- Document state in PostgreSQL is binary (Yjs update format). For search, + export, and web rendering, the API maintains a derived, plain + representation per page (see `data-model.md`, `page_content_cache`), + refreshed by the collab server's store hook. +- The collab server is stateless apart from in-memory open documents; it can + be restarted at any time (clients resync). Horizontal scaling would need + sticky routing or Redis pub/sub — out of scope at target size (ADR 0002). +- We accept the Yjs storage overhead (tombstones) — mitigated by snapshot + compaction (ADR 0013). + +## Alternatives considered + +- **Operational Transformation (e.g. ShareDB)**: mature for online-only + editing, weak offline story. Rejected because offline is a requirement. +- **Automerge**: viable CRDT, but the editor-binding ecosystem + (ProseMirror/TipTap) and server tooling around Yjs are significantly more + mature. +- **Self-written sync protocol**: never a good idea for this problem class. diff --git a/docs/architecture/adr/0004-tiptap-editor.md b/docs/architecture/adr/0004-tiptap-editor.md new file mode 100644 index 0000000..a915d93 --- /dev/null +++ b/docs/architecture/adr/0004-tiptap-editor.md @@ -0,0 +1,57 @@ +# ADR 0004: TipTap (ProseMirror) as the WYSIWYG editor + +- Status: accepted +- Date: 2026-07-04 + +## Context + +Dorfteich needs a WYSIWYG editor with: collaborative cursors, CRDT binding +(ADR 0003), Markdown-friendly copy/paste, image paste from the clipboard, +link editing UX (edit URL / open in new tab), and a schema that plugins can +extend with custom block types (ADR 0008). + +## Decision + +- **TipTap** (MIT-licensed core, built on ProseMirror) is the editor + framework, used with: + - `@tiptap/extension-collaboration` — binds the document to Yjs, + - `@tiptap/extension-collaboration-cursor` — remote cursors/selections, + - standard extensions for headings, lists, tables, code blocks, images, + links, task lists. +- **Document schema is the source of truth** and is defined centrally in + `packages/shared` (node/mark specs), so that editor, server-side + rendering/export, and plugin validation agree on what a valid document is. +- **Markdown interop**: paste and import/export convert between Markdown and + the ProseMirror document model (`prosemirror-markdown`, extended for our + custom nodes such as wikilinks). Markdown remains the primary exchange + format; the internal format is the ProseMirror/Yjs document, as the vision + allows ("internal format follows technical requirements"). +- **Links**: a bubble menu on links offers "edit URL" and "open in new tab" + (the two actions required by the vision). +- **Images**: clipboard paste and menu insert both upload through the API + (quota-checked, ADR 0011) and insert an image node referencing the stored + file — no base64 blobs inside documents. +- **Wikilinks**: a custom inline node `wikilink` with `[[` autocomplete, + resolved against pages of the current pond; backlinks are indexed + server-side (see `data-model.md`). + +## Consequences + +- ProseMirror's schema-based model gives us structural guarantees (valid + documents by construction) that plain contenteditable or Markdown-string + editors cannot. +- Plugin-defined block types register as ProseMirror nodes rendered inside a + sandbox (details in `plugin-architecture.md`). +- Mobile editing is explicitly out of scope (kickoff decision); we target + desktop browsers and accept degraded editing UX on touch devices, while + reading stays fully responsive. + +## Alternatives considered + +- **Slate, Lexical**: capable editors, but Yjs integration and the extension + ecosystem are notably less mature than ProseMirror/TipTap's. +- **CodeMirror + Markdown source editing**: excellent for developer wikis but + contradicts the WYSIWYG requirement. +- **BlockNote** (block editor on TipTap): attractive UX shortcut, but its + opinionated block model would constrain our plugin block types and styling + requirements; we build on TipTap directly. diff --git a/docs/architecture/adr/0005-react-vite-frontend.md b/docs/architecture/adr/0005-react-vite-frontend.md new file mode 100644 index 0000000..76d2113 --- /dev/null +++ b/docs/architecture/adr/0005-react-vite-frontend.md @@ -0,0 +1,51 @@ +# ADR 0005: React + Vite single-page application + +- Status: accepted +- Date: 2026-07-04 + +## Context + +The frontend hosts a heavily stateful collaborative editor (ADR 0003/0004), +a collapsible pond sidebar, admin UIs, and a plugin sandbox host. It must +work offline (PWA) and be maintainable by many independent contributors. +SEO for wiki content matters only for publicly readable pages; the separate +static project site (dorfteich.cloud) covers marketing needs. + +## Decision + +- **React (current stable) + Vite + TypeScript**, shipped as a static + single-page application served by the `web` container (nginx serving + `dist/`, with SPA fallback to `index.html`). +- Routing: React Router. Server state: TanStack Query. Local/UI state: + Zustand where component state is not enough. Forms: react-hook-form + Zod + schemas from `packages/shared`. +- Styling: CSS custom properties + a small utility layer; the visual design + is plain and professional per the vision. Pond-level font configuration + (ADR 0016) is applied via CSS variables. +- PWA: `vite-plugin-pwa` service worker caches the app shell for offline + editor startup (works with `y-indexeddb`, ADR 0003). +- **Public read-only pages are server-rendered for crawlers only where + needed**: the API exposes a plain HTML rendering endpoint per public page + (also used for PDF export, ADR 0009). We deliberately avoid SSR frameworks + for the app itself. + +## Consequences + +- Simple deployment (static files + API), no Node server for the frontend. +- TipTap's React bindings are first-class; the editor integration follows + the officially documented path. +- The SPA is the only consumer of the REST API, which keeps the API honest + as the boundary for self-hosted automation and future integrations. +- SEO for public wiki pages relies on the HTML rendering endpoint being + served to crawlers via reverse-proxy rules — documented in `deployment.md` + and acceptable for a wiki (dorfteich.cloud handles discoverability of the + product itself). + +## Alternatives considered + +- **SvelteKit**: excellent framework and used elsewhere in the operator's + projects, but the TipTap/Yjs collaborative-editing ecosystem, examples, + and collective experience are strongest in React; for a contributor-diverse + open-source project the larger ecosystem wins. +- **Next.js**: SSR/ISR complexity buys little for an app that is 95% behind a + login or served to a small community; PWA/offline is simpler in a pure SPA. diff --git a/docs/architecture/adr/0006-nestjs-prisma-backend.md b/docs/architecture/adr/0006-nestjs-prisma-backend.md new file mode 100644 index 0000000..a9d234d --- /dev/null +++ b/docs/architecture/adr/0006-nestjs-prisma-backend.md @@ -0,0 +1,54 @@ +# ADR 0006: NestJS + Prisma for the API server + +- Status: accepted +- Date: 2026-07-04 + +## Context + +The API server carries most business logic: auth, permission resolution, +pond/page/label CRUD, quotas, import/export orchestration, plugin +management, e-mail, admin functions. Stories are implemented by many +independent contributors in 0.5–2 day slices; the framework must make module +boundaries, dependency injection, validation, and testing conventions +explicit so parallel work does not collide. + +## Decision + +- **NestJS** (Express adapter) structures `apps/api` into feature modules + that mirror the domain: `auth`, `users`, `ponds`, `pages`, `labels`, + `permissions`, `search`, `uploads`, `import-export`, `plugins`, `quotas`, + `comments`, `notifications`, `admin`, `setup`, `mail`, `health`. +- **Prisma** is the ORM: schema-first data model (`schema.prisma` is the + single source of truth, mirrored in `data-model.md`), generated type-safe + client, `prisma migrate` for migrations (applied automatically at startup, + ADR 0002). Raw SQL is allowed where Prisma falls short (FTS queries, + ADR 0010; Yjs state upserts). +- API style: **REST + JSON** under `/api/v1`, request/response validated + with Zod schemas from `packages/shared` (single definition for client and + server). OpenAPI document generated from these schemas for documentation. +- Cross-cutting rules: + - Every route passes an authentication guard and a **permission guard** + that calls the shared resolution algorithm (`permissions.md`) — no + ad-hoc permission checks inside handlers. + - All external side effects (mail, conversion sidecars) go through + dedicated injectable services so tests can fake them. + +## Consequences + +- Stories can say "add endpoint X in module Y, guard with permission Z" and + be implemented without architectural decisions; NestJS's DI and testing + utilities give a uniform unit/e2e test pattern (Vitest + supertest). +- Prisma migrations serialize schema changes; stories touching the schema + must be sequenced (flagged in issue dependencies). +- NestJS adds some boilerplate per module; we accept this for the + consistency it buys in a many-contributors setting. + +## Alternatives considered + +- **Fastify/Express hand-rolled**: less boilerplate, but every contributor + invents structure; consistency would depend on discipline instead of + framework rails. Rejected for this team model. +- **tRPC**: excellent DX for a closed SPA+API pair, but a REST API is a + deliberate product feature for self-hosters and integrations. +- **Drizzle**: fine ORM, but Prisma's schema file + migration story is the + most widely known and the easiest to review. diff --git a/docs/architecture/adr/0007-auth-sessions-oidc-ready.md b/docs/architecture/adr/0007-auth-sessions-oidc-ready.md new file mode 100644 index 0000000..bdb42b0 --- /dev/null +++ b/docs/architecture/adr/0007-auth-sessions-oidc-ready.md @@ -0,0 +1,61 @@ +# ADR 0007: Cookie sessions, Argon2id, OIDC-ready identity model + +- Status: accepted +- Date: 2026-07-04 + +## Context + +Kickoff decisions: self-contained user management (username, e-mail, +password) with mandatory e-mail verification (double opt-in), password +reset, rate limiting, and the option to disable self-registration per +instance. SSO is not in the MVP but the design must allow adding OIDC login +later without schema surgery. + +## Decision + +### Authentication + +- **Server-side sessions** stored in PostgreSQL, referenced by an opaque + `HttpOnly; Secure; SameSite=Lax` cookie. No JWTs for browser sessions + (revocability and simplicity win). Sliding expiration, default 30 days. +- Passwords hashed with **Argon2id** (tuned parameters documented in code). +- **E-mail flows** (verification, password reset) use single-use, expiring, + hashed tokens; mail is sent via SMTP (instance-configured, see setup + wizard) through a mail outbox table with retry. +- **Rate limiting** on signup, login, password reset, and token endpoints: + fixed-window counters in PostgreSQL keyed by IP and by account — + no Redis (ADR 0002). +- Self-registration can be disabled instance-wide + (`instance_settings.registration_mode`: `open` | `closed`). The kickoff + also asked for the option of admin approval as a later hardening step; the + setting is an enum so `approval_required` can be added without migration + pain. +- **Collaboration tokens**: the API issues short-lived (≤ 60 s validity for + connect) signed JWTs solely for the WebSocket handshake with the collab + server (ADR 0003). These are the only JWTs in the system. + +### OIDC readiness (not in MVP) + +- The identity model separates **account** from **login method**: + `users` (profile, status) and `user_identities` + (`provider` = `password` | future `oidc:`, `subject`, + `credential`). Password login is just one identity row. +- E-mail is unique per user and verified; future OIDC linking matches on + verified e-mail or explicit account linking. + +## Consequences + +- Logout and account deactivation are immediate (session rows deleted). +- No shared secret sprawl: one signing key for collab tokens, rotated via + environment configuration. +- Adding OIDC later means: new identity provider rows, an + authorization-code flow module, and a login button — no changes to + sessions, permissions, or user references. + +## Alternatives considered + +- **JWT access/refresh tokens in the browser**: harder revocation, XSS + exposure of tokens, no benefit for a same-origin SPA. Rejected. +- **Auth libraries/services (Keycloak, Authentik as mandatory)**: heavy + extra container contradicting easy self-hosting; external IdPs remain + possible later through the OIDC path. diff --git a/docs/architecture/adr/0008-plugin-sandbox.md b/docs/architecture/adr/0008-plugin-sandbox.md new file mode 100644 index 0000000..23c6422 --- /dev/null +++ b/docs/architecture/adr/0008-plugin-sandbox.md @@ -0,0 +1,81 @@ +# ADR 0008: Sandboxed iframe plugins with a message-based API + +- Status: accepted +- Date: 2026-07-04 + +## Context + +The vision requires plugins that range from simple styling (colored section +backgrounds) to complex features (table of contents, page index, diagrams, +embedding blocks from other pages), installable at runtime (directory upload +or GUI) without redeploying. Kickoff decision: only Site Admins install +plugins, and plugins run **sandboxed** — an uploaded plugin must not be able +to compromise the server or exfiltrate data beyond what the viewing user may +see. + +## Decision + +- **Plugins are client-side packages only** (v1). No plugin code executes on + the server. A plugin is a ZIP containing: + - `manifest.json` — id, name, version, `apiVersion`, declared extension + points, declared permissions, i18n strings; + - `plugin.js` — a single ES module bundle; + - optional assets (CSS, images). +- **Two plugin classes, by trust needs:** + 1. **Declarative style plugins** — manifest + CSS only, no JavaScript. + They define named "section styles" (e.g. colored background boxes) + applied as attributes on standard container nodes. No sandbox needed; + CSS is served sanitized and scoped. + 2. **Code plugins** — run inside a **sandboxed `