|
All checks were successful
whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`. |
||
|---|---|---|
| .. | ||
| adr | ||
| audit-events.md | ||
| data-model.md | ||
| deployment.md | ||
| operations.md | ||
| permissions.md | ||
| plugin-architecture.md | ||
| README.md | ||
| realtime-collaboration.md | ||
| roadmap.md | ||
| security.md | ||
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).
flowchart LR
subgraph clients [Clients]
B[Browser SPA<br/>React + TipTap + Yjs]
end
subgraph instance [Dorfteich instance - Docker Compose]
RP[Reverse proxy]
WEB[web<br/>static SPA assets]
API[api<br/>NestJS REST]
COLLAB[collab<br/>Hocuspocus WebSocket]
PG[(PostgreSQL)]
PAN[pandoc-server<br/>doc conversion]
GOT[Gotenberg<br/>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 | TypeScript everywhere, pnpm monorepo |
| 0002 | PostgreSQL as the only database |
| 0003 | Yjs CRDT + Hocuspocus for real-time and offline collaboration |
| 0004 | TipTap (ProseMirror) as the WYSIWYG editor |
| 0005 | React + Vite single-page application |
| 0006 | NestJS + Prisma for the API server |
| 0007 | Cookie sessions, Argon2id, OIDC-ready identity model |
| 0008 | Sandboxed iframe plugins with a message-based API |
| 0009 | Pandoc + Gotenberg sidecars for import/export |
| 0010 | PostgreSQL full-text search behind a search interface |
| 0011 | Filesystem volume for uploads, DB-tracked quotas |
| 0012 | i18next with German and English from the start |
| 0013 | Page version history via Yjs snapshots, soft-delete trash |
| 0014 | CI/CD with Gitea Actions, staged promotion |
| 0015 | Nightly pg_dump + uploads sync, 30-day retention, off-host mirror |
| 0016 | Self-hosted Google Fonts, per-pond font configuration |
| 0017 | Accessibility (WCAG 2.1 AA) as a default requirement |
Concept documents
| Document | Contents |
|---|---|
| data-model.md | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins |
| permissions.md | Role model and the "most specific setting wins" resolution algorithm |
| realtime-collaboration.md | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks |
| plugin-architecture.md | Plugin manifest, packaging, sandbox runtime, extension points, admin flows |
| deployment.md | Compose stacks for Dev/Test/Int/Prod, environments, promotion pipeline |
| operations.md | Monitoring, logging, backup/restore, update strategy for self-hosters |
| security.md | Threat-driven security concept: authn/authz, sandboxing, uploads, secrets |
| 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.
- Every API route declares its access rule explicitly (a permission
decorator,
@Public(), or the Site-Admin guard — issue #52); permission checks run only through the shared resolution (permissions.md), never ad hoc. 404/403 policy: a denied read answers404so the existence of ponds and pages is not revealed; a denied write on something the user may read answers403. Trash views require write capability and answer404on denial (ADR 0013).