An instance had no way to look like itself: the top bar said "Dorfteich" 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"`. |
||
|---|---|---|
| .claude | ||
| .gitea/workflows | ||
| apps | ||
| deploy | ||
| docs | ||
| fixtures | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| .node-version | ||
| .prettierignore | ||
| .prettierrc.json | ||
| CLAUDE.md | ||
| eslint.config.mjs | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
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: https://dorfteich.cloud
- Public flagship instance: https://dorfteich.online
- License: MIT
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 upplus a guided first-run setup wizard yields a working instance. Start here:docs/self-hosting/README.md.
Documentation
- What is Dorfteich? —
docs/features.md - Manuals (user / pond admin / site admin / API / MCP) —
docs/manual/, auf Deutsch:docs/de/ - Extending it (plugins, core) —
docs/developer/extending.md - Running it —
docs/self-hosting/ - How it works inside —
docs/architecture/
Repository layout
| Path | Contents |
|---|---|
docs/manual/ |
User-facing manuals: user, pond-admin, site-admin, API, and MCP guides (start at docs/manual/README.md) |
docs/developer/ |
Extending Dorfteich: plugin development and core contributions |
docs/architecture/ |
Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
docs/self-hosting/ |
Install, update, backup, and troubleshooting guide for running your own instance |
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 |
Development
Requirements: Node.js ≥ 22 and pnpm (npm install -g pnpm).
pnpm install # install all workspace dependencies
pnpm lint # ESLint + Prettier check across the repo
pnpm typecheck # TypeScript --noEmit in every package
pnpm test # Vitest in every package
pnpm build # build every package (dependency order)
The workspace packages live under apps/ (web, api, collab) and
packages/ (shared). Shared logic goes into packages/shared and is
imported as @dorfteich/shared — never copy code between apps.
Status
Feature-complete for a 1.0: collaboration, permissions, import/export, plugins, public REST API + MCP, backups with off-host copies and in-app restore — all shipped and release-gated. Work is tracked as issues in this repository.
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.