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.

SDK v1.0Core ≥ 2.0Public BetaStand: Mai 2026

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)

BereichInhalt
AuthUser, Login, JWT, 2FA, GDPR-Helpers
CatalogProducts, Variants, Images, Categories, Tax-Classes
CommerceCart, Checkout, Orders, Stripe (primärer Payment-Provider)
CustomersAccounts, Adressen, Bestellhistorie
Settingsapp_settings Tabelle, AES-GCM Crypto, Admin-Credentials-UI
Email-BasicsResend-Wrapper, Order-Bestätigung, Versand-Benachrichtigung
Audit-Logzentrales Logging-System für sicherheitsrelevante Aktionen
Job-QueueDB-backed Background-Jobs mit Retry
Plugin-LoaderManifest-Parser, Hook-Bus, Settings-Discovery, Admin-UI-Slots

Offizielle Plugins (Stand Mai 2026)

PluginKategorieModell
sevdeskBuchhaltungPaid · ~€9/Mo
manufacturingProductionPaid · ~€19/Mo
multi-vendorMarketplacePaid · ~€29/Mo
affiliateMarketingPaid · ~€9/Mo
motorrad-fitmentNiche · MotorsportFree
csv-import-exportDataFree
riders-cmsContentFree
reviewsUGCFree
newsletterMarketingFree
bike-wish + cooperationLead-GenFree
cms-pagesContentFree
sendcloudShippingFree

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

FeldTypBeschreibung
displayNamestringUI-Name. Zeigt sich im Marketplace + Sidebar.
categoryenumbuchhaltung · marketing · shipping · content · production · marketplace · data · niche.
iconstringEmoji oder Pfad zu SVG (24×24).
coreVersionsemverWelche Core-Versionen sind kompatibel. Strict beim Loader.
hooksstring[]Liste von Events auf die das Plugin reagieren will. Loader registriert Listener.
routespathOptionaler Pfad zu einer Datei mit registerRoutes(app) Export.
adminPagesobject[]Sidebar-Einträge + Angular-Components für Admin-Erweiterung.
schemaobjectprefix = obligatorischer Tabellen-Prefix, migrations = Ordner mit numerierten .sql-Dateien.
jobsobject[]Background-Job-Handler. Core-Queue invoked sie nach Event.
settingsobject[]Settings-Keys die im Admin-UI automatisch als Form-Feld erscheinen.
billingobjectPricing-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.

PropertyTypWas
ctx.logLoggerstrukturiertes Logging (pino), automatisch mit Plugin-Name + Tenant-ID tagged.
ctx.hooksHookBus.on(event, handler) / .emit(event, payload).
ctx.dbDrizzleScopeRead-only-Zugriff auf alle Core-Tabellen, full-write auf eigene Prefix-Tabellen.
ctx.settingsSettingsService.get(key) / .set(key, value, opts) für die im Manifest deklarierten Keys.
ctx.jobsJobQueue.enqueue(type, payload, opts?) für asynchrone Tasks.
ctx.ordersOrdersAPItypisierte Wrapper über Order-Mutations — Plugins schreiben nicht direkt in Orders.
ctx.customersCustomersAPIanalog für Customer-CRUD.
ctx.productsProductsAPIanalog für Products.
ctx.emailEmailServiceTransactional-Email senden über zentralen Resend-Client.
ctx.auditAuditService.log({...}) — alles was du audit-loggen willst.
ctx.fetchFetchpreconfigured fetch mit Timeout + Retry. Vermeide raw fetch direkt — der Core-Logger trackt diesen.
ctx.tenantTenantInfoaktuelle 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

order.placedBestellung wurde im Checkout angelegt. Payload: { orderId, customerEmail, totalCents }.
order.paidZahlung via Stripe oder Banküberweisung als bezahlt markiert.
order.shippedSendcloud (oder anderes Plugin) hat die Order als versandt markiert.
order.deliveredTracking-Status reportet „delivered".
order.cancelledOrder vor Versand storniert.
order.refundedVoll- oder Teil-Refund durchgeführt.

Customer-Lifecycle

customer.registeredSelf-Register oder Admin-Invite abgeschlossen.
customer.updatedProfil oder Adresse geändert.
customer.password_resetPassword-Reset-Token wurde eingelöst.

Product & Inventory

product.createdNeues Produkt im Admin angelegt.
product.updatedInkl. Preis-, Status-, Image-Änderung.
product.deletedSoft- oder Hard-Delete.
inventory.low_stockStock unter konfiguriertem Threshold gefallen.

Cart & Checkout

cart.item_addedProdukt im Storefront in den Cart gelegt.
cart.discount_appliedRabattcode validiert + angewendet.
checkout.startedUser landet auf der Checkout-Page.
checkout.abandoned15 Min Inaktivität nach checkout.started ohne order.placed.

Auth & Settings

auth.loginErfolgreicher Login.
auth.login_failedFalsche Credentials, brute-force-relevant.
settings.changedSettings-Key wurde modifiziert. Cache-Invalidation-Signal.

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.

typeForm-ElementVerschlüsselt?
stringText-InputNein
secretPassword-Input mit „Wert anzeigen"-ButtonJa (AES-GCM)
numberNumber-InputNein
booleanToggleNein
selectDropdown (Options im Manifest)Nein
urlText-Input mit URL-ValidierungNein

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"-CTA
  • checkout.payment_methods — zusätzliche Payment-Provider
  • order_confirmation.below_summary — Affiliate-Plugin zeigt „Teile diesen Code"-Block

Publishing

Schritt-für-Schritt vom fertigen Plugin zum Marketplace-Eintrag:

  1. Repo öffentlich auf GitHub unter sellforge-plugins/<name>. Open-Source-Pflicht für alle offiziellen Plugins (Lizenz: MIT empfohlen).
  2. Pull-Request ansellforge-plugins/marketplace mit deinem Manifest. Review-Team checked Code + Security innerhalb von ~5 Werktagen.
  3. Nach Approval publiziert die CI das Build-Artifact in die private npm-Registry. Plugin erscheint im Marketplace-Tab im Admin.
  4. 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:

modelErklärung
freeImmer kostenlos. Open-Source-Bedingung gilt strikt.
subscriptionMonatliche Pauschale on-top zur Tenant-Subscription. Stripe-Subscription-Item wird automatisch hinzugefügt/entfernt.
usage_basedZ.B. €0.02 pro versandtem Newsletter. Plugin reportet Usage via ctx.billing.reportUsage(), abgerechnet wird zum Monatsende.
one_timeEinmal-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: true gesetzt 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.app mit:
    • 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.