dorfteich/docs/developer/extending.md
Claude Fable 5 2fe9374f16
All checks were successful
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CD / Build and push images (push) Successful in 17s
Release / Build release images and notes (push) Successful in 1m18s
CD / Smoke tests against Test (push) Successful in 1m18s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 59s
CI / Build container images (pull_request) Successful in 3m49s
CI / Auth e2e pack (pull_request) Successful in 6m47s
CD / Deploy to Test (push) Successful in 11s
CD / Promote to Int (push) Successful in 11s
Release / Release-candidate operations QA (push) Successful in 45s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 4m36s
CI / Auth e2e pack (push) Successful in 6m54s
Doku: Excalidraw als sechstes Standard-Plugin ergänzt
- Tutorial K19: neue Sektion „Excalidraw — Skizzen wie von Hand" mit
  Nadias Bühnenplan-Beispiel (konzerte-live); Intro fünf→sechs Plugins.
- Tutorial K18: fünf→sechs Standard-Plugins.
- Site-Admin-Guide (en+de): Referenzliste + Build-Namensliste +
  Build-Hinweis (~16-MiB-ZIP aus npm).
- plugin-architecture.md: Referenz-Eintrag excalidraw (npm-Library-
  Spielart des Bundled-App-Pfads, {scene, svg}).
- developer/extending (en+de): Excalidraw als zweite Bundled-App-Variante.

Holt die in #136 zugesagten Doku-Ergänzungen nach. Screenshot für K19
folgt nach dem CSP-Deploy (damit die Handschrift korrekt rendert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 22:27:17 +02:00

6.9 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. The excalidraw plugin shows the other flavor of the same idea: an npm React library bundled directly into plugin.js (esbuild) and mounted fullscreen in the frame itself, with its runtime assets (fonts, locales) shipped in the ZIP. 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/shared with a comment saying who reads and who writes.