dorfteich/docs/architecture/README.md
Claude Fable 5 0c6494f209
All checks were successful
CD / Build and push images (push) Successful in 2m54s
CI / Lint, typecheck, test (push) Successful in 2m25s
CI / Auth e2e pack (push) Successful in 2m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 12s
Enforce permissions in API guards and retire interim access (#52)
Every route now declares its access rule explicitly and is enforced
through the shared resolution algorithm (permissions.md):

- PermissionGuard + decorators (@RequiresPondRole, @RequiresPagePermission,
  @RequiresAttachmentPermission, @AuthenticatedOnly) applied to every
  route; a route-enumeration test proves full coverage alongside
  @Public()/Site-Admin-guarded routes.
- 404/403 policy (documented in README conventions): denied reads answer
  404 (existence hiding), denied writes on readable things answer 403;
  trash views need write capability (ADR 0013).
- PermissionService resolves page/pond questions via the shared resolver,
  with an in-process pond-context cache (grants + label parents) that is
  invalidated on every grant/label-tree change and TTL-bounded as a
  multi-process safety net. Grant changes also fire pond_access_changed
  for collab revalidation (#39/#53).
- shared: pond-scope resolution (hasPondRole, canSeePond) next to the
  page resolver; grant wire schemas + GrantView.
- Owner Pond-Admin grants: migration backfill for all existing ponds,
  created transactionally with every new pond (shared + personal + seed).
- Grant CRUD under /ponds/:id/grants (pond_admin-gated) with structural
  and referential validation, last-admin protection, audit logs.
- InterimAccessService deleted; page lists, search, backlinks, phantom
  links, and trash listings are filtered per page through the resolver;
  collab tokens are now truly ro for readers.
- Fixture-matrix e2e (reader/editor/pond admin/foreign, label-deny,
  authenticated-subject, revoke-then-immediate-deny cache test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 16:31:41 +02:00

113 lines
6.6 KiB
Markdown

# 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<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](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.
- 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_ answers `404` so the existence
of ponds and pages is not revealed; a denied _write_ on something the
user may read answers `403`. Trash views require write capability and
answer `404` on denial (ADR 0013).