Any authenticated user can invite an e-mail address; the mailed single-use token lets exactly one signup through even while registration is closed. Open (pending, unexpired) invitations count against the new instance setting invitations.maxOpenPerUser (default 5, 0 disables inviting) — plus a 20/day per-user rate limit so a revoke-and-recreate loop cannot become a mail cannon. Only the SHA-256 token hash is stored (auth-tokens pattern); a failed signup (taken username) un-redeems the token so the invitee can retry. Surfaces: invitations section in the user settings (list, invite, revoke, quota line; wide table in a focusable .table-scroll region), signup page reads ?invitation=<token> (preview banner, e-mail prefill, closed-mode gate opens only for a previewed-valid token), admin general card gets the quota field (flat RHF name per #322; VS-NfD marked and hideable). Governance: audit actions invitation.created/revoked/accepted (catalogue 1.10), VS-NfD profile entry (compliant: 0) + hardening-guide row, i18n de+en including the invitation mail template. Tests: api e2e-db (mail link, closed-mode single-use signup with un-redeem on failure, quota + revoke frees slot, quota 0 = 403, auth matrix), new web e2e pack invitations.spec.ts (full UI loop through Mailpit, wired into ci.yml with its own rate-limit reset), a11y scan waits for the new section. Full api suite (107 files / 607 tests), auth/admin-settings/a11y packs green against a fresh local stack. Closes #332 |
||
|---|---|---|
| .. | ||
| adr | ||
| audit-events.md | ||
| data-model.md | ||
| deployment.md | ||
| operations.md | ||
| permissions.md | ||
| plugin-architecture.md | ||
| pond-archive-format.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).