Marketplace — Plugin-Store (Ist-Stand)
Status: Phasen 1-4 LIVE (2026-05-01). Vorgaengerdokument: process-engine-phase-10-marketplace.md (Konzept).
Der Plugin-Store ist Prilogs zentraler Verteilungs-Mechanismus fuer Flows, Anleitungen und Apps. Tenants installieren mit einem Klick (oder mit Confirm-Dialog bei Bezahl-Items), Drittanbieter reichen Bundles via API ein, Prilog reviewt und published, Stripe wickelt Zahlung und Vendor-Auszahlung ab.
Architektur-Ueberblick
┌─────────────────────────┐
│ marketplace_items │
│ (Catalog, published) │
└────────────┬────────────┘
│ 1..n
▼
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ Tenant │ │ Stripe-Subscription│ │ Vendor │
│ installiert │──│ + transfer_data │──│ Connect-Account │
└─────────────────┘ └─────────────────────┘ └──────────────────┘
▲
│ submission → review → approve
│
┌─────────────────────────┐
│ marketplace_submissions│
│ (Vendor → Prilog) │
└─────────────────────────┘Drei Haupt-Entitaeten:
MarketplaceItem— ein installierbares Item (Flow oder App).status: draft|published|archived. Hat optional Pricing + Stripe-Price-ID + Vendor.MarketplaceVendor— Drittanbieter (oder Prilog selbst). Stripe-Connect-Express-Account, charges/payouts-Status, Provision (%).MarketplaceSubmission— eingereichtes Bundle pending Review. Wird beim Approve in einMarketplaceItemueberfuehrt.
Plus: TenantMarketplaceSubscription — bildet ab, welcher Tenant was installiert hat. Speichert die stripeSubscriptionId (separate Sub pro Item, damit Connect-Splits per-Item gehen).
Datenbank-Schema
marketplace_items
id VARCHAR(50) PK
item_type VARCHAR(20) -- 'flow' | 'app'
slug VARCHAR(80) UNIQUE
name VARCHAR(120)
description TEXT
icon_emoji VARCHAR(10)
category VARCHAR(40) -- 'crisis' | 'organization' | 'education' | 'communication' | 'management' | 'tools' | 'operations' | 'other'
vendor_id VARCHAR(50) → marketplace_vendors.id
vendor_name VARCHAR(120) -- denormalisiert
version VARCHAR(20)
status VARCHAR(20) -- 'draft' | 'published' | 'archived'
price_model VARCHAR(20) -- null | 'flat-monthly' | 'per-active-user'
price_cents INTEGER
stripe_product_id VARCHAR(120) -- on-demand erzeugt bei Pricing
stripe_price_id VARCHAR(120)
bundle JSONB -- { templateExport: ... } für Flows | { manifest: ... } für Apps
screenshots JSONB -- []
published_at TIMESTAMPTZ
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZmarketplace_vendors
id VARCHAR(50) PK
name VARCHAR(120)
contact_email VARCHAR(255)
country VARCHAR(2) -- ISO-2, default 'DE'
stripe_account_id VARCHAR(120) UNIQUE -- Connect Express
stripe_account_status VARCHAR(30) -- 'pending' | 'active' | 'restricted'
charges_enabled BOOLEAN
payouts_enabled BOOLEAN
application_fee_percent NUMERIC(5,2) -- default 30 (Prilog)
created_at, updated_atmarketplace_submissions
id, item_type, name, description, icon_emoji, category, version
vendor_id → marketplace_vendors.id
vendor_name, vendor_email
price_model, price_cents
bundle JSONB
status VARCHAR(20) -- 'pending' | 'approved' | 'rejected' | 'needs-changes'
review_notes TEXT
reviewed_at, reviewed_by
approved_item_id → marketplace_items.idtenant_marketplace_subscriptions
id, tenant_id, item_id
installed_version VARCHAR(20)
stripe_subscription_id VARCHAR(120) -- die separate Sub pro Item
stripe_subscription_item_id VARCHAR(120) -- erstes Item der Sub
status VARCHAR(20) -- 'active' | 'cancelled'
installed_at, cancelled_at
installed_template_id VARCHAR(50) -- bei Flow-Install: id des kopierten Templates
UNIQUE (tenant_id, item_id)Migrations: 0025_marketplace, 0026_marketplace_paid, 0027_marketplace_submissions.
Bundle-Formate
Flow-Bundle
{
"templateExport": {
"format": "prilog.process-engine/v1",
"template": { "appKind": "guide", "name": "...", "description": "...", "metadata": {} },
"components": [{ "_localId": "s1", "kind": "guide.screen", "label": "...", "config": {}, "sortOrder": 0 }],
"edges": [{ "sourceLocalId": "s1b", "targetLocalId": "s2", "condition": { "type": "always" }, "sortOrder": 0 }]
}
}Beim Install ruft installItem importTemplateForTenant() auf — selbe Logik wie POST /process/templates/import. Ergebnis: ein neues ProcessTemplate mit Components + Edges im Tenant.
App-Bundle
{
"manifest": {
"moduleKey": "prilog-project",
"featureFlag": "project",
"permissions": ["spaces:read", "files:read", "files:write"]
}
}Beim Install sucht installItem die moduleRegistration mit dem passenden manifest.featureFlag und legt eine tenant_module_installation an. Voraussetzung: das Modul muss schon im Backend bekannt sein (also moduleRegistration-Eintrag existiert). Drittanbieter-Apps mit eigenem Code-Bundle brauchen Sandbox-Architektur — siehe "Was bewusst (noch) nicht geht" weiter unten.
Endpoints
Tenant-Sicht (/api/platform/v1/marketplace/*, JWT-Auth)
| Method | Path | Zweck |
|---|---|---|
| GET | /marketplace/items?type=flow|app[&category=...] | Catalog der published Items + installed-Flag |
| GET | /marketplace/items/:id | Detail mit Bundle |
| POST | /marketplace/items/:id/install | installiert Item, legt ggf. Stripe-Sub an |
| GET | /marketplace/subscriptions | Was hat dieser Tenant installiert |
| DELETE | /marketplace/subscriptions/:id | Uninstall (Stripe-Sub gecancelt, Template/Modul-Daten bleiben in Karenz) |
Public-Submission-Flow (/api/public/marketplace/*, kein Login)
| Method | Path | Zweck |
|---|---|---|
| POST | /marketplace/submissions | Vendor reicht Bundle ein (Rate-Limit 5/Std/IP) |
| GET | /marketplace/submissions/:id | Vendor checkt Status (pending/approved/rejected/needs-changes) |
Admin (/api/admin/marketplace/*, Admin-JWT)
| Method | Path | Zweck |
|---|---|---|
| GET | /marketplace/vendors | Vendor-Liste |
| POST | /marketplace/vendors | Vendor + Stripe-Connect-Account anlegen |
| POST | /marketplace/vendors/:id/onboarding-link | Hosted-Onboarding-URL (einmalig) |
| POST | /marketplace/vendors/:id/sync | Status manuell von Stripe ziehen |
| GET | /marketplace/items | Alle Items (auch draft/archived) |
| PATCH | /marketplace/items/:id | Status / Pricing / Vendor aendern → loest Stripe-Price-Rotation aus |
| GET | /marketplace/submissions[?status=...] | Submission-Liste |
| GET | /marketplace/submissions/:id | Detail |
| POST | /marketplace/submissions/:id/approve | erzeugt marketplace_items + Stripe-Price |
| POST | /marketplace/submissions/:id/reject | (mit Begruendung) |
| POST | /marketplace/submissions/:id/request-changes | (mit Begruendung) |
Lifecycle-Flows
Install (Flow, kostenlos)
Tenant click "Installieren"
→ POST /marketplace/items/:id/install
→ installItem() ruft importTemplateForTenant()
→ 2-Pass-Component-Import (Pass 1: ohne groupId; Pass 2: groupId-Mapping)
→ Edges nach idMap
→ tenant_marketplace_subscriptions upsert (status=active, installedTemplateId=<neue tplId>)
→ Web-Client navigiert zu /flows/:installedTemplateIdInstall (Flow oder App, kostenpflichtig + Vendor)
Tenant click "Installieren"
→ Confirm-Dialog mit Preis ("X €/Monat")
→ POST /marketplace/items/:id/install
→ installItem() prueft:
- Vendor.stripeAccountStatus === 'active' && chargesEnabled? sonst VENDOR_INACTIVE
- Tenant.stripeCustomerId != null? sonst NO_STRIPE_CUSTOMER
→ ensureItemStripePrice(itemId)
- Erstellt Stripe Product + Price wenn nicht da, persistiert IDs
- flat-monthly: recurring/month, licensed
- per-active-user: recurring/month, metered, sum aggregation
→ stripe.subscriptions.create({
customer: tenant.stripeCustomerId,
items: [{ price: priceId }],
transfer_data: { destination: vendor.stripeAccountId }, // NUR mit Vendor
application_fee_percent: vendor.applicationFeePercent
})
→ Bundle-spezifische Aktion (Flow: importTemplateForTenant; App: tenant_module_installation upsert)
→ tenant_marketplace_subscriptions speichert stripeSubscriptionIdUninstall
Tenant click Trash-Icon
→ confirm() ("Bezahl-Item wird sofort gekuendigt")
→ DELETE /marketplace/subscriptions/:id
→ uninstall():
- if stripeSubscriptionId: stripe.subscriptions.cancel()
- if app: tenant_module_installations.deactivated (30d-Karenz via Modul-Lifecycle-Cron)
- if flow: kopiertes Template bleibt — Tenant darf weiternutzen
- tenant_marketplace_subscriptions.status = 'cancelled'Submission-Review
Vendor → POST /api/public/marketplace/submissions
→ validateSubmission() (Stufe A: Schema, Bundle-Format, Pricing-Plausibilitaet)
→ marketplace_submissions.status = 'pending'
→ Email an Vendor: "Submission eingegangen, Review innerhalb 2 Werktagen"
→ Admin sieht im Submissions-Tab (mit Pending-Counter-Badge)
Admin oeffnet Review-Modal:
- Sieht Beschreibung, Vendor-Status, Pricing, Bundle (raw JSON)
- Drei Aktionen mit Notes-Feld:
- Approve → erzeugt marketplace_items (slug aus Name + Konflikt-Suffix)
→ ensureItemStripePrice() wenn paid
→ Email an Vendor: "Live im Store"
- Aenderungen anfragen → status='needs-changes' + Email
- Reject → status='rejected' + Email (Begruendung Pflicht)Stripe-Integration
Setup-Voraussetzungen
Tenant hat Pro-Subscription:
- tenant.stripeCustomerId != null
- via existing Pro-Onboarding (3 €/Aktiv-User/Monat)
Vendor wurde angelegt:
- createVendor(): stripe.accounts.create({ type: 'express', country: 'DE', email })
- vendor.stripeAccountId gesetzt, stripeAccountStatus = 'pending'
Vendor durchlaeuft Onboarding:
- Admin generiert Hosted-Link via createOnboardingLink()
- Vendor: KYC + IBAN bei Stripe
- Webhook 'account.updated' → syncVendorStatus() schreibt charges_enabled / payouts_enabled
- status='active' sobald beides truePricing-Modell-Mapping
| Item-Pricing | Stripe-Price | Cron-Aufwand |
|---|---|---|
null (kostenlos) | kein Price | — |
flat-monthly (z.B. 5 €/Mt) | recurring/month, licensed | — (Stripe billed automatisch) |
per-active-user (z.B. 0,50 €/User/Mt) | recurring/month, metered, sum | marketplace-monthly-billing Cron |
Per-Active-User-Cron
Schedule: 30 6 1 * * (1. um 06:30 UTC, 30 min nach Pro-Cron)
Job: marketplace-monthly-billing
Was passiert:
- findet alle aktiven Subs mit item.priceModel='per-active-user'
- countActiveUsers(tenantId, lastMonth) — Wiederverwendung des Pro-Counters
- stripe.subscriptionItems.createUsageRecord({ quantity, action: 'set' })Webhook-Handler
POST /api/webhooks/stripe (existing) verteilt:
| Event | Handler |
|---|---|
customer.subscription.created/updated (mit metadata.tenantId) | freemium-billing.handleStripeSubscriptionEvent |
customer.subscription.deleted (mit metadata.marketplaceItemId) | Marketplace-Sub als cancelled markieren |
customer.subscription.deleted (sonst) | Light-Order-Suspend (existing) |
account.updated (Vendor-Connect) | syncVendorStatus → vendors.charges_enabled / payouts_enabled |
Vendor-Auszahlung
Stripe wickelt selbststaendig ab via transfer_data.destination:
Tenant zahlt 10,00 € → Stripe Customer
├─ application_fee 30 % = 3,00 € → Prilog-Account
└─ Rest 7,00 € → Vendor Connect-Account → woechentliche Auszahlung an IBANRefunds, Tax (KStG/Umsatzsteuer), 1099-Form: alles Stripe.
Bootstrap & Seeds
bootstrapMarketplace() laeuft beim Server-Start (idempotent):
- Flow-Seeds (
seeds/flow-seeds.ts): 5 vorgefertigte Anleitungen —notfall-brand,onboarding,faq,wiedereinstieg,klassenfahrt. Werden published mit vendorName='Prilog'. - App-Seeds (
seeds/app-seeds.ts): 1 App —project-module. Wird nur published wenn entsprechendemoduleRegistrationmit demfeatureFlagexistiert (sonst Skip).
Manuell archivierte Items (status='archived' im Admin) werden NIE ueberschrieben.
Code-Struktur
prilog-backend-api/src/marketplace/
├── bootstrap.ts # Boot-Hook fuer Seeds
├── seeds/
│ ├── flow-seeds.ts # 5 Flow-Templates
│ └── app-seeds.ts # 1 App
├── import-template.ts # Geteilte Logik mit POST /templates/import
├── marketplace.service.ts # listPublishedItems, installItem, uninstall
├── stripe-price.service.ts # ensureItemStripePrice, rotateItemStripePrice
├── stripe-connect.service.ts # createVendor, createOnboardingLink, syncVendorStatus
├── submission.service.ts # validateSubmission, approve/reject/requestChanges + Email
└── marketplace-billing.service.ts # per-active-user Cron-Logik
prilog-backend-api/src/routes/
├── platform-v1/marketplace.router.ts # Tenant-API
├── admin/marketplace.router.ts # Admin-API (Vendors, Items, Submissions)
└── public/marketplace-submission.router.ts # Public-Submission
prilog-web-client/src/features/
├── flows/marketplace-gateway.ts # API-Client
└── settings/sections/plugins-section.tsx # Plugin-Store-UI (Tab in Settings)
prilog-admin/src/app/marketplace/
└── page.tsx # 3-Tab-UI (Submissions/Items/Vendors)Was bewusst (noch) nicht geht
Drittanbieter-Apps mit eigenem Backend-Code
App-Items aktivieren heute nur Module die schon im Prilog-Backend existieren. Echte Drittanbieter-Apps mit eigenem Code-Bundle (z.B. ZIP mit JS-Dateien) brauchen eine Sandbox-Architektur (V8-Isolates oder WebWorker oder Container-Isolation pro App). Das ist Wochen Arbeit, nicht Tage — aufschieben bis erster realer Drittanbieter ankommt.
Vendor-Self-Service-Submission im Web
Heute reichen Vendors via POST /api/public/marketplace/submissions ein (curl, eigenes Tool, oder ein Mini-Onboarding-UI das spaeter gebaut werden kann). Eine vollwertige Vendor-Konsole (Login, Bundle-Upload-Wizard, Erloese-Sicht, History) ist eigene Arbeit — heute reicht der Email-Submission-Flow.
Vendor-Erloese-Dashboard
Vendor sieht heute nur via Stripe-Express-Dashboard (Stripe-Hosted) seine Auszahlungen. Eine Prilog-eigene Sicht "wer hat dein Plugin installiert, welche Erloese" waere ein Phase 2c-Feature.
Versions-Updates fuer schon installierte Items
Heute bekommt ein Tenant der v1 installiert hat NICHT automatisch v2 wenn Vendor das Item updated. Das wuerde eine Update-Notification + Migrate-Logik brauchen (Schema-Diff, Conflict-Resolution). Defer'd bis es Bedarf gibt.
Code-Bundle-Storage in S3
Aktuell stehen alle Bundles inline in marketplace_items.bundle (JSONB). Fuer Flows reicht das (paar KB). Wenn App-Code-Bundles (ZIPs, mehrere MB) kommen, braucht es S3-Storage — Schema ist vorbereitet (kann bundle.codeBundleUrl werden).
Tester-Workflow
Eigenes Premium-Plugin
admin.prilog.chat/marketplace→ Items-Tab.- Ein Item editieren, Pricing auf "Flat / Monat / 5 €" setzen, speichern.
- Im Web-Client als Tenant: Settings → Plugin-Store → Install → Confirm-Dialog mit Preis.
- Wenn Tenant Pro-Sub hat: Stripe-Subscription wird angelegt. Dashboard zeigt sie.
Vendor-Onboarding
admin.prilog.chat/marketplace→ Vendors-Tab → "Vendor anlegen".- Name, Email, Land. Submit.
- Auf neuem Vendor "Onboarding-Link" → oeffnet Stripe-Hosted-KYC im neuen Tab → Vendor durchlaeuft Onboarding.
- Stripe Webhook synct status zurueck. Admin "Sync"-Button als Fallback wenn Webhook nicht ankommt.
Submission
curl -X POST https://api.prilog.chat/api/public/marketplace/submissions \
-H 'Content-Type: application/json' \
-d '{
"itemType": "flow",
"name": "Mein Test-Flow",
"description": "Was er tut",
"iconEmoji": "🎯",
"category": "organization",
"vendorName": "Acme Corp",
"vendorEmail": "x@example.com",
"priceModel": "flat-monthly",
"priceCents": 500,
"bundle": {
"templateExport": {
"format": "prilog.process-engine/v1",
"template": { "appKind": "guide", "name": "Test", "description": "" },
"components": [],
"edges": []
}
}
}'→ Vendor bekommt Confirmation-Email. Admin sieht im Submissions-Tab (Pending-Badge im Sidebar). Approve erzeugt das Item.
Anschlussfaehig an
- Process-Engine-App-Architektur — Components-Plugin-System, das Flow-Bundles im
templateExport-Format vorschreibt. - Freemium-Modell — die Pro-Subscription liefert den
stripeCustomerIdder fuer Marketplace-Subs noetig ist. - Process-Engine Phase-10-Konzept — die ursprungliche Marketplace-Vision aus Phase 9.