Mirrors the English tree (docs/de/{features.md,manual/*,developer/
extending.md}) so relative links between translated guides resolve
within the German set; links into untranslated areas (self-hosting,
architecture, deploy) point at the English files and say so. Every
quoted UI label matches the actual German interface strings. Each
pair of files cross-links the other language; English stays
authoritative when the two diverge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
6.7 KiB
Developer guide — extending Dorfteich
Deutsche Fassung: docs/de/developer/extending.md
Two ways to make Dorfteich do more: write a plugin (no fork, no redeploy, safe by construction) or contribute to the core. Start with a plugin unless you need to change how the product itself works.
Writing a plugin
Read docs/architecture/plugin-architecture.md
once — it is the contract. The short version:
A plugin is a ZIP with a manifest.json, a single ES-module bundle
plugin.js, optional styles.css, i18n/*.json, and assets/…. It
contributes one or more extension points:
| Type | You build | Example |
|---|---|---|
sectionStyle |
named CSS styles for content sections (no code at all) | section-styles-basic |
pageTool |
a read-only widget in the page-tools panel | toc, page-index |
block |
a custom editor block with its own data and edit UI | mermaid, drawio |
The sandbox — what your code can and cannot do
Your plugin.js runs in an <iframe sandbox="allow-scripts"> with an
opaque origin and a strict CSP: no cookies, no storage, no parent DOM,
and network/frames only to your own bundled assets — never to the api
or any external host. Everything else goes through the typed RPC the
SDK provides, executed with the viewing user's permissions:
import { createPlugin, windowTransport } from '@dorfteich/plugin-sdk';
const { host } = createPlugin({
transport: windowTransport({
target: { postMessage: (m) => window.parent.postMessage(m, '*') },
source: window,
}),
onRender: async (ctx) => {
const outline = await host.readCurrentPage.getOutline();
document.body.textContent = outline.map((e) => e.text).join('\n');
void host.ui.resize(document.body.scrollHeight + 16);
},
onEdit: async (ctx) => {
/* block plugins: editing UI; persist via host.blockData.setData(...) */
},
});
Capabilities you may declare in the manifest permissions and what they
unlock: readCurrentPage (outline/content/meta), readPond (page
lists + contents), readBlock (cross-page block reads), blockData
(your block's getData/setData — writes become normal document
changes, replicated and versioned), ui (resize, openPage, toast,
scrollToHeading, enterFullscreen/exitFullscreen). Calls outside
the declared set are rejected at runtime.
Block plugins in three sentences
The host mounts your frame per block and calls render (view) or
edit (the user pressed the block's edit button). Persist
{ …yourData } via host.blockData.setData — collaborators' frames
re-render live when the data changes under them. Store a static
snapshot (e.g. an svg string) alongside your source data: exports and
the public view show it through the manifest fallback machinery
without ever executing plugin code.
Bundled apps and fullscreen
A plugin may ship an entire sub-application as assets and run it in a
child iframe of its own asset path — that is how the drawio plugin
embeds the real draw.io editor. Combine with
host.ui.enterFullscreen() for editors that need the whole screen.
Size limits: 64 MiB ZIP, 256 MiB unpacked.
Developing and shipping
The reference plugins under packages/plugins/ are the templates —
copy the closest one. Each has a build.mjs that bundles src/plugin.ts
with esbuild and packs the installable ZIP into dist/:
cd packages/plugins/<your-plugin>
pnpm build # → dist/<id>-<version>.zip
Install the ZIP via Admin → Plugins (or the dropzone), open the
sandboxed preview at /admin/plugins/<id>/preview, iterate. The install
gate validates structure, manifest, size and CSS scoping and rejects
with a precise error code. Publishing an update = same id, higher
version.
Conventions that will be enforced on review: UI strings via the plugin's
i18n/ files in both de and en; the fallback must make sense in
a printed document.
Working on the core
Stack at a glance
TypeScript monorepo (pnpm workspaces): apps/api (NestJS + Prisma,
PostgreSQL), apps/collab (Hocuspocus/Yjs realtime server),
apps/web (React + Vite + TipTap), apps/backup (backup sidecar),
packages/shared (types, schemas, editor schema, i18n catalogs),
packages/plugin-sdk, packages/plugins/*. Architecture decisions live
in docs/architecture/adr/ — read the relevant
ADR before touching a subsystem; docs/architecture/ has the deep dives
(permissions, data model, realtime collaboration, security, plugins).
Getting a dev environment
pnpm install
cd deploy/compose && cp .env.example .env
# Full containerized dev stack (hot reload; first start installs deps):
docker compose -f docker-compose.yml -f compose.dev.yml up
# → web http://localhost:5173, api :3001, db :5434
Fastest feedback: run only the database in Docker and web/api natively —
the exact recipe is documented at the top of
deploy/compose/compose.dev.yml.
Seed fixture users/ponds with pnpm --filter @dorfteich/api db:seed
(fixture password: see apps/api/prisma/seed.ts).
The gates every change must pass
pnpm lint # ESLint + Prettier — no pipes that swallow exit codes
pnpm typecheck
pnpm test # vitest everywhere; DB-backed suites need TEST_DATABASE_URL
pnpm i18n:check # every UI string in de AND en
DB-backed tests run against the compose dev database:
TEST_DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm test.
Playwright e2e packs live in apps/web/e2e/ and run against a seeded
local stack (see .gitea/workflows/ci.yml for the exact recipe).
House rules worth knowing before your first PR
- Permissions: never answer an access question outside
PermissionService/the route decorators; denied reads are 404, denied writes on readable things are 403 (permissions.md). - i18n: no hard-coded UI strings; add keys to
packages/shared/i18n/{de,en}/…(ADR 0012). - No third-party requests from the product, ever — fonts, editors, everything ships self-hosted (ADR 0016 sets the precedent).
- Migrations: additive and reversible within one minor release; the release pipeline's QA gate replays an upgrade from the previous release against real data.
- Document contracts: anything two services share (status files,
NOTIFY channels, wire types) lives in
packages/sharedwith a comment saying who reads and who writes.