Ein Modul ist die Code-Einheit hinter jedem Tool: ein eigenständiger Ordner mit festem Vertrag. Diese Seite führt dich vom Scaffold bis zum validierten, dokumentierten Modul.
Ordner-Struktur
Jedes Modul lebt unter apps/app/src/modules/<id>/. Das Mindest-Set:
apps/app/src/modules/<id>/
├── manifest.ts # Single Source of Truth (ID, Kosten, Permissions, UI, Lifecycle)
├── compute.ts # exportiert runTool (ModuleCompute), der einzige Harness-Eintrag
├── ui/
│ └── index.tsx # Tool-Komponente (Default-Export, ModuleToolProps)
├── lib/ # modul-interne Utilities, nie Cross-Module-Imports
├── lifecycle/
│ ├── on-install.ts # optionaler Hook (Default-Export, ModuleLifecycleHook)
│ └── on-uninstall.ts
├── docs/
│ ├── index.de.mdx # Tool-Doku, gespiegelt in die Docs-Site
│ └── index.en.mdx
├── tests/ # alle Modul-Tests leben IM Modul
├── assets/ # icon.svg + Marketplace-Bilder
├── CHANGELOG.md # user-facing, wird im Marketplace angezeigt
└── README.md # Entwickler-Notizen zum ModulEin Modul ist eine Black-Box für andere Module: Cross-Module-Imports sind verboten. Kommunikation läuft über Platform-Events oder geteilte Platform-Entities.
Scaffold und Registrierung
bun create:module --name=<id>Das Kommando klont das _template-Modul und ersetzt Platzhalter. Danach drei Schritte, die kein Script für dich erledigt:
- Registry: Das Modul in
apps/app/src/modules/registry.tseintragen (Manifest + Lazy-Loader fürui/,compute.ts,lifecycle/). Der Validator erzwingt: Ordner und Registry-Eintrag gehören zusammen. - Permissions:
tools.use.<id>(und bei Settings-Paneltools.configure.<id>) in der SDK-Permission-Registry deklarieren.bun validate:permissionsgleicht jedesmanifest.permissions[]-Entry gegen die Registry ab. - Doku:
docs/index.{de,en}.mdxfüllen undbun sync:module-docslaufen lassen (siehe unten).
Das Manifest
manifest.ts ist die einzige Datei im Modul-Root und wird beim Laden gegen das Schema geparst:
import { type ModuleManifest, ModuleManifestSchema } from "@reosa/sdk";
const manifest: ModuleManifest = ModuleManifestSchema.parse({
id: "homestaging", // kebab-case, muss dem Ordnernamen entsprechen
version: "0.1.0", // semver
displayName: "AI Homestaging",
category: "visualisierung", // marketing | visualisierung | texterstellung | ...
shortDescription: "...", // max 280 Zeichen, Marketplace-Kachel
longDescription: "...", // Marketplace-Detailseite
minimumPlan: "compact", // compact | standard | max | max-plus
creditsPerUse: 8, // 0 nur bei aiRiskClass "none" oder pricing "addon"
pricing: "credits", // "credits" (Default) oder "addon"
aiRiskClass: "minimal", // none | minimal | limited | high | prohibited
status: "coming_soon", // erst auf "available" schalten, wenn das Tool real ist
ui: { tool: true }, // + optional settingsPanel, chrome, openWith
lifecycle: { onInstall: "lifecycle/on-install" },
permissions: ["tools.use.homestaging"],
marketplaceListing: { visibility: "public", tags: ["fotos"] },
});
export default manifest;Wichtige Regeln:
statusstartet aufcoming_soon. So geht ein frisch gescaffoldetes Modul nie versehentlich live;coming_soonrendert nur die Marketing-Kachel.marketplaceListingträgt die Marketplace-Darstellung (Sichtbarkeit, Summary, Tagline, Highlights, Use-Cases). Alles optional, die Detailseite rendert nur vorhandene Blöcke.availableInTrial(Defaulttrue) schaltet Trial-Workspaces aus, wenn dein Tool nach außen zeigende Flächen hat (z.B. öffentliche Buchungslinks), die nach Trial-Ende sterben würden.- Lifecycle-Referenzen (
lifecycle/on-install) sind Datei-Pfade ohne Endung; der Struktur-Validator prüft, dass die Files existieren.
Compute: der Runs-Harness-Vertrag
Dein Modul exportiert genau eine Funktion. Der Harness ruft sie auf und besitzt alles drumherum (Credits, Run-Zeile, Notification, Webhook-Events):
// compute.ts
import type { ModuleCompute } from "@reosa/sdk";
import { z } from "zod";
const inputSchema = z.object({ style: z.string(), imageKey: z.string() });
export const runTool: ModuleCompute = async (ctx, { payload, propertyId }) => {
const input = inputSchema.parse(payload); // Zod zuerst, payload ist unknown
const result = await generate(ctx, input); // die eigentliche Arbeit
return {
output: { generationId: result.id }, // landet auf tool_runs.result
summary: "Dein Homestaging ist fertig.", // eine Zeile für die Notification
resultHref: `/tools/homestaging?g=${result.id}`,
};
};- Werfen genügt. Wirft
runTool, erstattet der Harness die Credits, markiert den Lauf als fehlgeschlagen und benachrichtigt die Nutzer:in. Kein eigenes Refund-Handling im Modul. - Die UI bekommt den Invoker als Prop. Die Tool-Komponente erhält
runTool(einModuleRunInvoker) überModuleToolProps; Module importieren nie Server-Actions direkt, der Struktur-Validator blockt@/server/...-Imports. - Das Ergebnis für die UI ist ein
ModuleRunOutcome:{ ok: true, runId, output, creditsUsed }oder{ ok: false, error, errorCode }mit Codes wieinsufficient_credits,forbidden,invalid_input.
Lifecycle-Hooks
lifecycle/on-install.ts und lifecycle/on-uninstall.ts exportieren je eine ModuleLifecycleHook-Funktion ((ctx) => Promise<void>). Die Install-/Uninstall-Actions rufen sie fail-soft auf: ein werfender Hook bricht die Installation nicht ab, verhindert also keine Deinstallation. Typische Aufgaben: Default-Einstellungen anlegen, tool-eigene Daten aufräumen.
Validierung
Vor jedem PR lokal laufen lassen, CI prüft dasselbe:
bun validate:module <id> # alle Checks für ein Modul gebündelt
bun validate:manifests # jedes Manifest gegen das Schema
bun validate:module-structure # Ordner-Konventionen + Datei-Referenzen
bun validate:permissions # manifest.permissions[] gegen die RegistryDoku im Modul
Tool-Doku lebt im Modul unter docs/<page>.<locale>.mdx (flach, index.de.mdx ist die Landing) und wird gespiegelt:
bun sync:module-docs # spiegelt in apps/docs/src/content/tools/
bun sync:module-docs --check # CI-Drift-GuardDer Sync validiert die Frontmatter (Section muss tools sein, Pflicht-Keys vorhanden) und markiert die Ziel-Dateien als auto-synced: bearbeitet wird immer im Modul, nie in apps/docs. Eine Phase gilt erst als fertig, wenn die Tool-Doku geschrieben, frontmatter-valid und indexiert ist.
CHANGELOG: user-facing, ohne Interna
Die CHANGELOG.md deines Moduls sehen Immobilienmakler:innen im Marketplace. Deshalb:
- Jeder Eintrag beschreibt, was sich für die Nutzer:in ändert, nie wie es implementiert ist. Richtig: "DB-Leads ist jetzt ein Add-on-Tool." Falsch: "Manifest pricing addon, Migration 0076 rewritet Altwerte."
- Verboten im Changelog: Dateinamen, Funktions- und Komponenten-Namen, Manifest-Felder, Migrations-Nummern, Env-Variablen. Technik gehört in Commit-Messages und Architektur-Docs.
- Struktur: Keep-a-Changelog-Kategorien (Added/Changed/Fixed), ein Eintrag pro spürbarer Änderung, Deutsch, kurze Sätze. Rein interne Refactorings bekommen keinen Eintrag.
Weiterführend
Die SDK-Namespaces, die dein Compute typischerweise nutzt: Credits, Dateispeicher, REOS AI, Notifications, Audit und Webhooks und Events. Zurück zum Überblick.