Plugin SDK
Erweitere Sellforge — vom Loyalty-Programm bis zur ERP-Anbindung — als unabhängiges npm-Package mit klarem Vertrag und automatisierter Verteilung über den offiziellen Marketplace.
Introduction
Der Sellforge-Core ist absichtlich schlank gehalten — Catalog, Cart, Checkout, Stripe, Customer-Accounts, Settings und Audit-Log. Alles andere — Buchhaltungs-Sync, Multi-Vendor-Marketplace, Affiliate-Programme, Manufacturing-Module, Newsletter, Reviews, Country-spezifische Integrationen — lebt als separates Plugin.
Das hat drei Vorteile:
- Minimal-System — neue Shops starten ohne ungenutzte Tabellen, Routen, Settings-Müll. Was nicht aktiviert ist, läuft auch nicht.
- Wartbarkeit — jedes Plugin hat seinen eigenen Repo, Test-Suite und Release-Zyklus. Bug in der sevdesk-Integration? Fix nur dort.
- Marktplatz-Logik — Free-Plugins sind Open-Source und können von der Community verbessert werden. Paid-Plugins finanzieren die Wartung — entweder als Subscription-Add-On oder im höheren Tier inklusive.
Architektur-Philosophie
Plugins laufen in-process als dynamisch geladene TypeScript-Module — kein RPC-Layer, kein WebAssembly-Sandbox. Das gibt dir volle Power: Fastify-Routes registrieren, eigene DB-Tabellen anlegen, Job-Handler hinzufügen, Settings ablegen, Admin-UI-Elemente einklinken.
Im Gegenzug verlangt der Marketplace strikte Review: jeder Code-Pfad wird vor Release angeschaut, alle Plugins sind auf GitHub Open-Source einsehbar, und die register()-Funktion läuft mit einem klar definierten Context-Objekt, das nur die explizit erlaubten Core-Services kennt.
In-Process trade-off: ein bösartiges Plugin könnte theoretisch alles. Deshalb existiert kein offener Marketplace — nur du und zertifizierte Partner publishen, alles Open-Source, Reviews vor jedem Release. Wenn dein Plugin eigene Daten verarbeitet (z.B. ERP-Sync), bleibt die Verantwortung beim Plugin-Author.
Was bleibt Core, was wird Plugin
Klare Trennlinie — alles im Core ist in jedem Tier verfügbar, alles als Plugin ist optional aktivierbar:
Core (immer dabei)
| Bereich | Inhalt |
|---|---|
| Auth | User, Login, JWT, 2FA, GDPR-Helpers |
| Catalog | Products, Variants, Images, Categories, Tax-Classes |
| Commerce | Cart, Checkout, Orders, Stripe (primärer Payment-Provider) |
| Customers | Accounts, Adressen, Bestellhistorie |
| Settings | app_settings Tabelle, AES-GCM Crypto, Admin-Credentials-UI |
| Email-Basics | Resend-Wrapper, Order-Bestätigung, Versand-Benachrichtigung |
| Audit-Log | zentrales Logging-System für sicherheitsrelevante Aktionen |
| Job-Queue | DB-backed Background-Jobs mit Retry |
| Plugin-Loader | Manifest-Parser, Hook-Bus, Settings-Discovery, Admin-UI-Slots |
Offizielle Plugins (Stand Mai 2026)
| Plugin | Kategorie | Modell |
|---|---|---|
| sevdesk | Buchhaltung | Paid · ~€9/Mo |
| manufacturing | Production | Paid · ~€19/Mo |
| multi-vendor | Marketplace | Paid · ~€29/Mo |
| affiliate | Marketing | Paid · ~€9/Mo |
| motorrad-fitment | Niche · Motorsport | Free |
| csv-import-export | Data | Free |
| riders-cms | Content | Free |
| reviews | UGC | Free |
| newsletter | Marketing | Free |
| bike-wish + cooperation | Lead-Gen | Free |
| cms-pages | Content | Free |
| sendcloud | Shipping | Free |
Quick Start
In fünf Minuten von leerem Verzeichnis zum lauffähigen Plugin:
# 1. Scaffold npx @sellforge/create-plugin my-loyalty cd my-loyalty # 2. Lokal entwickeln (linked gegen den Core) npm install npm run dev # 3. In einem zweiten Terminal: Core mit Plugin laden cd ../sellforge-core npm link ../my-loyalty npm run dev # 4. Im Admin-UI auf "Plugins" → "Installiert" — dein Plugin steht da
Der Scaffold erzeugt ein Plugin-Skelett:
my-loyalty/ ├── package.json # mit smotofactory-Block ├── src/ │ ├── index.ts # register() — Entry-Point │ ├── routes.ts # Fastify-Routes (optional) │ ├── jobs/ │ │ └── award-points.ts# Background-Jobs (optional) │ ├── admin/ │ │ └── page.ts # Angular-Standalone-Component (optional) │ └── schema/ │ └── 001_init.sql # DB-Migrationen mit Plugin-Prefix ├── tsconfig.json └── README.md
Plugin-Manifest
Im package.json deines Plugins lebt ein Sellforge-spezifischer Block. Hier ein realistisches Beispiel:
{
"name": "@sellforge-plugin/sevdesk",
"version": "1.2.0",
"main": "./dist/index.js",
"type": "module",
"sellforge": {
"displayName": "sevDesk-Integration",
"description": "Automatischer Rechnungs-Sync zu sevDesk inkl. Zahlungsbestätigung.",
"category": "buchhaltung",
"icon": "📊",
"coreVersion": "^2.0",
"hooks": ["order.placed", "order.paid", "order.cancelled", "order.refunded"],
"routes": "./dist/routes.js",
"adminPages": [
{ "path": "/sevdesk", "label": "sevDesk", "icon": "📊", "component": "./dist/admin/page.js" }
],
"schema": { "prefix": "sevdesk_", "migrations": "./dist/schema" },
"jobs": [
{ "type": "sevdesk.sync_invoice", "handler": "./dist/jobs/sync-invoice.js" }
],
"settings": [
{ "key": "sevdesk.api_token", "type": "secret", "label": "API-Token" },
{ "key": "sevdesk.user_id", "type": "string", "label": "sevDesk User-ID" },
{ "key": "sevdesk.tax_rule_id", "type": "string", "default": "1" }
],
"billing": {
"model": "subscription",
"price_eur_month": 9,
"free_in_tiers": ["forge"]
}
}
}Felder im Detail
| Feld | Typ | Beschreibung |
|---|---|---|
| displayName | string | UI-Name. Zeigt sich im Marketplace + Sidebar. |
| category | enum | buchhaltung · marketing · shipping · content · production · marketplace · data · niche. |
| icon | string | Emoji oder Pfad zu SVG (24×24). |
| coreVersion | semver | Welche Core-Versionen sind kompatibel. Strict beim Loader. |
| hooks | string[] | Liste von Events auf die das Plugin reagieren will. Loader registriert Listener. |
| routes | path | Optionaler Pfad zu einer Datei mit registerRoutes(app) Export. |
| adminPages | object[] | Sidebar-Einträge + Angular-Components für Admin-Erweiterung. |
| schema | object | prefix = obligatorischer Tabellen-Prefix, migrations = Ordner mit numerierten .sql-Dateien. |
| jobs | object[] | Background-Job-Handler. Core-Queue invoked sie nach Event. |
| settings | object[] | Settings-Keys die im Admin-UI automatisch als Form-Feld erscheinen. |
| billing | object | Pricing-Modell. Siehe Billing. |
Entry-Point: register()
Jedes Plugin exportiert eine register-Funktion. Der Core ruft sie beim Aktivieren genau einmal mit dem Plugin-Context auf:
// src/index.ts import type { PluginContext } from "@sellforge/sdk"; import { syncInvoice, bookPayment } from "./sevdesk"; export async function register(ctx: PluginContext) { ctx.log.info("sevdesk plugin activated"); // Auf Order-Events reagieren ctx.hooks.on("order.placed", async (e) => { await ctx.jobs.enqueue("sevdesk.sync_invoice", { orderId: e.orderId }); }); ctx.hooks.on("order.paid", async (e) => { await bookPayment(ctx, e.orderId, e.totalCents); }); // Optional: explicit teardown wenn das Plugin deaktiviert wird return { async deactivate() { ctx.log.info("sevdesk plugin deactivated"); } }; }
Plugin-Context
Das einzige Objekt das dein Plugin vom Core kriegt. Stellt typed Wrapper für alle Sellforge-internen Services bereit. Direkte Imports aus dem Core-Repo sind verboten — du verlierst sonst Forward-Compat-Garantien.
| Property | Typ | Was |
|---|---|---|
| ctx.log | Logger | strukturiertes Logging (pino), automatisch mit Plugin-Name + Tenant-ID tagged. |
| ctx.hooks | HookBus | .on(event, handler) / .emit(event, payload). |
| ctx.db | DrizzleScope | Read-only-Zugriff auf alle Core-Tabellen, full-write auf eigene Prefix-Tabellen. |
| ctx.settings | SettingsService | .get(key) / .set(key, value, opts) für die im Manifest deklarierten Keys. |
| ctx.jobs | JobQueue | .enqueue(type, payload, opts?) für asynchrone Tasks. |
| ctx.orders | OrdersAPI | typisierte Wrapper über Order-Mutations — Plugins schreiben nicht direkt in Orders. |
| ctx.customers | CustomersAPI | analog für Customer-CRUD. |
| ctx.products | ProductsAPI | analog für Products. |
| ctx.email | EmailService | Transactional-Email senden über zentralen Resend-Client. |
| ctx.audit | AuditService | .log({...}) — alles was du audit-loggen willst. |
| ctx.fetch | Fetch | preconfigured fetch mit Timeout + Retry. Vermeide raw fetch direkt — der Core-Logger trackt diesen. |
| ctx.tenant | TenantInfo | aktuelle Tenant-ID, Subscription-Tier, Locale. |
Lifecycle
Plugin-Status-Maschine im Core:
installed → enabled → active ↑ ↓ └─ uninstall ── disabled ← deactivate
- installed: npm-Install gelaufen, Migrationen lokal vorhanden, aber Hooks noch nicht registriert. Settings sind leer.
- enabled: User klickt im Admin „Aktivieren". Core ruft
register()auf. Wenn Settings unvollständig sind, bleibt das Plugin in einem Warnzustand und Hooks feuern nicht. - active: Settings vollständig, Hooks fired, Routes erreichbar, Admin-Pages sichtbar.
- disabled:
deactivate()wird aufgerufen, Hooks abgehängt, Sidebar-Link verschwindet. Daten in den Plugin-Tabellen bleiben. - uninstall: 30-Tage Soft-Delete der Plugin-Tabellen, Stripe-Subscription-Item wird gekürzt. Hard-Delete passiert dann automatisch.
Hooks (Event-Bus)
Standardisierte Events die der Core publiziert. Jeder Hook hat ein typisiertes Payload. Async-Handler werden parallel ausgeführt, Fehler in einem Handler kratzen andere nicht.
Order-Lifecycle
{ orderId, customerEmail, totalCents }.Customer-Lifecycle
Product & Inventory
Cart & Checkout
checkout.started ohne order.placed.Auth & Settings
Eigene Events kannst du ebenfalls feuern und in anderen Plugins lauschen. Namensraum nach Plugin-Name: "sevdesk.invoice_synced", "loyalty.points_awarded".
HTTP-Routes
Plugins können eigene Fastify-Routes registrieren. Convention: /api/plugins/<plugin-name>/... für Plugin-spezifische Endpoints. Admin-only-Routes leben unter /api/admin/plugins/<plugin-name>/....
// src/routes.ts import type { FastifyInstance } from "fastify"; import type { PluginContext } from "@sellforge/sdk"; export async function registerRoutes(app: FastifyInstance, ctx: PluginContext) { app.get("/api/plugins/loyalty/me/points", { preHandler: ctx.auth.requireCustomer(), }, async (req) => { const points = await ctx.db.query.loyaltyPoints.findFirst({ where: (t, { eq }) => eq(t.customerId, req.user.id), }); return { points: points?.balance ?? 0 }; }); }
Route-Registration läuft nach dem Hook-Setup und vor dem ersten Request. CORS, Rate-Limit, Auth-Middleware werden vom Core-Stack automatisch angewendet.
DB-Schema
Plugin-Tabellen MÜSSEN den Prefix aus manifest.schema.prefix tragen. Migrationen leben in einem Ordner als numerierte .sql-Dateien:
my-loyalty/src/schema/ ├── 001_init.sql ├── 002_add_tier_table.sql └── 003_index_on_customer_id.sql
Beispiel-Migration:
-- 001_init.sql CREATE TABLE "loyalty_points" ( "id" text PRIMARY KEY, "customer_id" text NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, "balance" integer NOT NULL DEFAULT 0, "updated_at" timestamp with time zone NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX "loyalty_points_customer" ON "loyalty_points" ("customer_id");
Boundary-Regel: Plugins dürfen Core-Tabellen nur lesen. Schreibzugriff auf orders, products, users et al. läuft ausschließlich über ctx.orders.*, ctx.products.*, ctx.customers.*. Hintergrund: Direkter SQL-Write umgeht Audit-Log, Hook-Triggering und Validation.
Job-Queue
Lange laufende Operationen (z.B. Sync zu externer API) gehören in die Job-Queue. Plugin registriert Job-Handler im Manifest, Core kümmert sich um Retry-Logic + Exponential-Backoff.
// src/jobs/sync-invoice.ts import type { JobHandler, PluginContext } from "@sellforge/sdk"; import { syncInvoice } from "../sevdesk"; export const handler: JobHandler<{ orderId: string }> = async (payload, ctx: PluginContext) => { const order = await ctx.orders.get(payload.orderId); if (!order) throw new Error("order not found"); await syncInvoice(ctx, order); }; export const options = { maxAttempts: 5, backoff: "exponential", // 1m, 2m, 4m, 8m, 16m timeout: 30_000, // 30s hard-limit pro Versuch };
Job auslösen:
await ctx.jobs.enqueue("sevdesk.sync_invoice", { orderId: "ord_abc" });
Settings
Settings die das Plugin braucht werden im Manifest deklariert. Core rendert automatisch passende Form-Felder im Admin-UI unter Einstellungen → Zugangsdaten. Secrets werden AES-GCM-verschlüsselt mit dem Tenant-Master-Key gespeichert.
| type | Form-Element | Verschlüsselt? |
|---|---|---|
| string | Text-Input | Nein |
| secret | Password-Input mit „Wert anzeigen"-Button | Ja (AES-GCM) |
| number | Number-Input | Nein |
| boolean | Toggle | Nein |
| select | Dropdown (Options im Manifest) | Nein |
| url | Text-Input mit URL-Validierung | Nein |
Zugriff aus dem Plugin:
const token = await ctx.settings.get("sevdesk.api_token"); if (!token) throw new Error("Plugin nicht konfiguriert");
Admin-UI Extension Points
Plugins liefern Angular-Standalone-Components als ES-Module. Core lädt sie lazy beim Aufruf der Route:
Sidebar-Entry
Vollwertige Admin-Page mit eigener Route. Manifest deklariert adminPages[], Core baut Sidebar-Link.
Settings-Sektion
Aus Manifest-settings[] automatisch gerendert. Kein eigener Code nötig.
Order-Detail-Widget
Ein Widget in der Sidebar von /orders/:id einklinken — z.B. „sevDesk-Sync-Status" mit Retry-Button.
Product-Detail-Tab
Zusätzlicher Tab im Stepper-Layout, analog zum eingebauten „Passt auf"-Tab.
Dashboard-KPI
Eigene KPI-Kachel auf der Dashboard-Page mit Live-Chart.
Bulk-Aktion
Zusätzliche „Action"-Buttons in Listen-Pages (z.B. „CSV-Export"-Button in /orders).
// src/admin/page.ts — Angular-Standalone-Component import { Component, signal } from "@angular/core"; import type { SellforgeAdminContext } from "@sellforge/admin-sdk"; @Component({ selector: "app-sevdesk-page", standalone: true, template: `<h1>sevDesk-Status</h1>...`, }) export class SevdeskPageComponent { constructor(private ctx: SellforgeAdminContext) {} // Zugriff auf API + Audit-Log + Toast über ctx.* }
Storefront-UI
Storefront-Erweiterungen laufen analog: das Plugin kann Slots an definierten Stellen befüllen. Beispiele für aktuelle Slots:
product.detail.below_price— z.B. Loyalty-Plugin zeigt „Bekomme 79 Punkte für diesen Kauf"cart.summary.below_total— z.B. Reviews-Plugin zeigt einen „Bewerte deine letzte Bestellung"-CTAcheckout.payment_methods— zusätzliche Payment-Providerorder_confirmation.below_summary— Affiliate-Plugin zeigt „Teile diesen Code"-Block
Publishing
Schritt-für-Schritt vom fertigen Plugin zum Marketplace-Eintrag:
- Repo öffentlich auf GitHub unter
sellforge-plugins/<name>. Open-Source-Pflicht für alle offiziellen Plugins (Lizenz: MIT empfohlen). - Pull-Request an
sellforge-plugins/marketplacemit deinem Manifest. Review-Team checked Code + Security innerhalb von ~5 Werktagen. - Nach Approval publiziert die CI das Build-Artifact in die private npm-Registry. Plugin erscheint im Marketplace-Tab im Admin.
- Tenants installieren per Klick. Auto-Update via Semver-Range (Minor + Patch automatisch, Major nur mit Bestätigung).
Billing
Pricing-Modelle die das SDK unterstützt:
| model | Erklärung |
|---|---|
| free | Immer kostenlos. Open-Source-Bedingung gilt strikt. |
| subscription | Monatliche Pauschale on-top zur Tenant-Subscription. Stripe-Subscription-Item wird automatisch hinzugefügt/entfernt. |
| usage_based | Z.B. €0.02 pro versandtem Newsletter. Plugin reportet Usage via ctx.billing.reportUsage(), abgerechnet wird zum Monatsende. |
| one_time | Einmal-Kauf. Plugin bleibt lebenslang aktiv, keine wiederkehrenden Kosten. |
Mit free_in_tiers kannst du dein Plugin in höheren Subscription-Tiers inklusive machen — z.B. ist sevDesk im Forge-Tier ohne Aufpreis dabei, in den kleineren Tiers kostet es €9/Mo extra.
Revenue-Split: Plugin-Author bekommt 70 % der Plugin-Subscription, Sellforge behält 30 % als Marketplace-Fee. Auszahlung monatlich via Stripe-Connect. Für Free-Plugins keine Auszahlung — die finanzierst du selbst oder über Maintenance-Sponsorships.
Versioning & Compatibility
Plugins folgen strikt SemVer:
- Patch (1.0.x): Bugfix, kein Breaking-Change. Wird im Admin automatisch installiert.
- Minor (1.x.0): Neue Features, abwärtskompatibel. Automatisch installiert wenn
autoUpdate: truegesetzt ist. - Major (x.0.0): Breaking-Change. User muss im Admin manuell bestätigen, Migration-Guide ist Pflicht im Release.
Plugin deklariert im Manifest welche Core-Versionen es unterstützt: coreVersion: "^2.0". Der Loader blockt die Aktivierung wenn der Core eine inkompatible Version hat — du siehst dann eine klare Fehlermeldung im Admin.
Certified-Partner-Programm
Wer publishen darf:
- Sellforge-Team selbst (alle initialen offiziellen Plugins).
- Zertifizierte Partner nach Bewerbung. Bewerbung läuft über
partners@sellforge.appmit:- 2 öffentliche GitHub-Repos als Code-Sample (TypeScript bevorzugt)
- Beschreibung des geplanten Plugins
- Geschäftliche Identität (für Stripe-Connect-Onboarding)
Nach Approval kriegst du Publish-Rechte auf die private npm-Registry und einen Slack-Channel mit Core-Team für Q&A.
Security & Review
Vor jedem Release wird das Plugin geprüft auf:
- Keine Hardcoded-Secrets oder API-Keys im Code
- Keine eval-/Function-Constructor-/Dynamic-Code-Execution-Patterns
- Keine ungeprüften
child_process-Aufrufe - Keine direkten SQL-Writes auf Core-Tabellen
- Saubere TypeScript-Strict-Mode-Compilation
- Audit-Log bei allen Mutations
- Realistische Test-Coverage für die Hook-Handler
Bei Security-Findings nach Release: Plugin wird sofort marketplace-weit deaktiviert, Partner muss innerhalb 48h einen Patch liefern.
Testing
Das SDK liefert einen Test-Helper, der einen Mock-Context und eine Test-Postgres-Instanz bereitstellt:
// tests/sync-invoice.test.ts import { describe, it, expect } from "vitest"; import { createTestContext } from "@sellforge/sdk/testing"; import { register } from "../src"; describe("sevdesk plugin", () => { it("queues sync job on order.placed", async () => { const ctx = await createTestContext({ settings: { "sevdesk.api_token": "test-token" }, }); await register(ctx); await ctx.hooks.emit("order.placed", { orderId: "ord_test", totalCents: 5000 }); expect(ctx.jobs.enqueued).toContainEqual( expect.objectContaining({ type: "sevdesk.sync_invoice" }), ); }); });
Beispiele
Lerne aus existierenden offiziellen Plugins — alle Open-Source unter github.com/sellforge-plugins:
- sevdesk — komplexes Plugin: Hooks, Jobs, Settings, Admin-Page mit Retry-UI, Stripe-Connect-Integration
- affiliate — Multi-Hook-Tracking, DB-Schema mit Earnings + Payouts, Storefront-Slot-Integration
- csv-import-export — UI-fokussiertes Plugin mit Listen-Page-Action-Buttons
- motorrad-fitment — Niche-Plugin mit komplexem DB-Schema und Product-Detail-Tab
FAQ
Kann mein Plugin ein anderes Plugin als Dependency haben?
Ja, über manifest.requires: ["@sellforge-plugin/affiliate"]. Core blockt die Aktivierung, wenn die Dependency fehlt, und prüft Versionkompatibilität.
Kann ich Plugin-Code privat halten?
Nein — offizielle Plugins müssen Open-Source sein. Wenn du proprietäre Logik integrieren willst, leg einen externen Service auf (z.B. AWS Lambda) und ruf den von deinem Plugin per HTTP auf. Die API-Credentials gehen in die Plugin-Settings.
Was passiert wenn der Core eine Breaking-Change-Version released?
Alle Plugins mit coreVersion: "^1.0" bleiben auf der alten Core-Version aktiv. Tenants müssen erst alle ihre Plugins auf 2.0-kompatible Versionen aktualisieren, bevor das Core-Update durchgeht. Wir kündigen Major-Releases mindestens 8 Wochen vorher an.
Können Plugins die Kasse (Checkout) ändern?
Ja, über zwei Wege: Slots in der Checkout-UI für UI-Erweiterungen, und Hooks (checkout.before_place_order) für Validierungs-Logik. Wenn ein Plugin im before_place_order-Handler einen Fehler wirft, wird die Bestellung abgelehnt.
Gibt's Rate-Limits für Plugin-Routes?
Standard ist 300 Requests pro Minute pro IP (geerbt vom Core). Du kannst pro Route über die Fastify-Optionen lockern ({ config: { rateLimit: { max: 1000 } } }), Maximum ist 5000/min, dann blockt der Core hart.
Wo kriege ich Support?
Erst GitHub Discussions. Für zertifizierte Partner zusätzlich der private Slack-Channel.