dorfteich/docs/developer/extending.md
Claude Fable 5 eeb0ef6794
All checks were successful
CD / Build and push images (push) Successful in 1m9s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m7s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m34s
CI / Import/export fidelity gate (push) Successful in 47s
Documentation set: features, manuals (user/pond-admin/site-admin), API, MCP, developer guide
Seven audience-targeted documents (English first, German translation to
follow), linked from the README and a new docs/manual/ index:

- docs/features.md — public-facing feature overview: what Dorfteich
  can do and why that matters
- docs/manual/user-guide.md — everyday use: editor, wikilinks, labels,
  search, comments, watches/digests, import/export, settings
- docs/manual/pond-admin-guide.md — pond configuration: members/roles,
  access rules incl. label scoping and public pages, labels, comment
  policy, plugins, API/MCP opt-ins, files, export
- docs/manual/site-admin-guide.md — instance administration: wizard,
  settings, quotas, uploads, API/MCP switches, legal pages, plugins,
  users, and the system panel (jobs/backups/audit/storage)
- docs/manual/api-guide.md — example-driven public-API walkthrough
  (tokens, reading, writing through the collab-safe path, labels,
  comments, error semantics)
- docs/manual/mcp-guide.md — connecting AI assistants: switches, token
  scopes, Claude Code one-liner, mcp-remote bridge, tool table, audit
  and safety properties
- docs/developer/extending.md — plugin development (sandbox contract,
  SDK, block plugins, bundled apps/fullscreen, shipping) and core
  contributions (stack, dev environment, gates, house rules)

README: documentation index, repository-layout rows for docs/manual and
docs/developer, and the stale "architecture phase" status brought up to
reality. All relative links verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 17:18:30 +02:00

6.6 KiB

Developer guide — extending Dorfteich

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/shared with a comment saying who reads and who writes.