Onboarding & Shared Hosting
Prilog bietet zwei Hosting-Modelle an. Kunden waehlen beim Onboarding zwischen Shared (Starterpaket) und Dedicated (eigener Server). Ein Kunde kann mehrere Instanzen beider Typen gleichzeitig betreiben.
Hosting-Modelle
| Shared — Starterpaket | Dedicated | |
|---|---|---|
| Nutzer | Bis 20 | Unbegrenzt |
| Preis | 34,50 EUR/Monat inkl. MwSt. | Ab 45 EUR Basis + 3 EUR/Nutzer |
| Server | Geteilter Host (Multi-Tenant) | Eigener Hetzner-Server |
| Isolation | DB + Bucket + Synapse-Prozess getrennt | Komplett eigener Server |
| n8n | Nicht verfuegbar | Eigene n8n-Instanz |
| Provisionierung | ~2-5 Minuten | ~15-30 Minuten |
| Subdomain | Freitext, z.B. turnverein-linden.prilog.team | Freitext, z.B. weser.prilog.team |
| Zielgruppe | Vereine, Kitas, kleine Einrichtungen | Schulen, Traeger, grosse Organisationen |
Onboarding-Flow
Der Bestellvorgang laeuft ueber onboarding.prilog.chat in 6 Schritten:
Schritt 1: Hosting-Typ + Subdomain
├── Shared oder Dedicated waehlen
└── Wunsch-Subdomain eingeben (Live-Verfuegbarkeitspruefung)
Schritt 2: Rechnungsadresse
└── Firma, Strasse, PLZ, Stadt, Land, USt-ID
Schritt 3: Ansprechpartner
└── Vorname, Nachname, E-Mail, Telefon
Schritt 4: Admin-Zugang
└── Benutzername + Passwort (min. 12 Zeichen, Gross/Klein/Zahl/Sonderzeichen)
Schritt 5: Tarif & Zahlung
├── Shared: Fixpreis 34,50 EUR, nur Kreditkarte, Trial-Hinweis
└── Dedicated: Nutzer-Slider → automatische Tier-Auswahl + Preisberechnung
Schritt 6: Rechtliches
├── AGB, Datenschutz, AV-Vertrag akzeptieren
└── Preis-Zusammenfassung (Shared: Brutto + Trial-Info, Dedicated: Netto + MwSt.)
Nach Absenden:
├── Shared: Redirect zu Stripe Checkout → Karte hinterlegen → Success-Seite
└── Dedicated: Bestaetigungs-Seite → E-Mail mit ZugangsdatenSubdomain-Regeln
- Mindestens 3, maximal 30 Zeichen
- Nur Kleinbuchstaben, Zahlen und Bindestriche
- Muss mit Buchstabe oder Zahl beginnen und enden
- Live-Pruefung gegen Blocklist und bestehende Bestellungen
- Kann nach Erstellung nicht geaendert werden
Multi-Instanz-Faehigkeit
Ein Kunde kann mehrere Instanzen bestellen — mit derselben E-Mail-Adresse.
Beispiel:
- Schulverband Weser: 1x Dedicated (Hauptschule) + 2x Shared (Kita, Jugendtreff)
- Turnverein Linden: 1x Shared (Vorstand) + 1x Shared (Jugendabteilung)
Login-Flow bei mehreren Instanzen
Kunde gibt E-Mail + Passwort ein
│
├── 1 Instanz gefunden → direkt Token + Dashboard (wie bisher)
│
└── Mehrere Instanzen → Auswahl-Liste:
┌─────────────────────────────────────────┐
│ Grundschule Weser [Dedicated] │
│ weser.prilog.team │
├─────────────────────────────────────────┤
│ Kita Sonnenschein [Shared] │
│ kita-sonnenschein.prilog.team │
├─────────────────────────────────────────┤
│ Jugendtreff Nord [Shared] │
│ jugendtreff-nord.prilog.team │
└─────────────────────────────────────────┘
Kunde waehlt → Token fuer diese Instanz → DashboardTechnische Umsetzung
POST /api/customer/login: Prueft allePortalAccount-Eintraege fuer die E-Mail- Bei >1 Match: Antwort mit
multiInstance: trueundinstances[](kein Token) - Kunde waehlt Instanz in der UI
POST /api/customer/select-instance: Verifiziert Passwort erneut, gibt JWT-Token fuer die gewaehlte Instanz zurueck- JWT enthaelt
tenantId,currentOrderId,portalAccountId— alle Folge-Requests sind an diese Instanz gebunden
Shared-Hosting Architektur
Infrastruktur
*.prilog.team (Wildcard-DNS)
│
▼
┌─────────────────────────────────────────────────┐
│ Shared Host (z.B. CX32) │
│ │
│ Nginx (Wildcard-Cert, Host-basiertes Routing) │
│ │ │
│ ├── kita-sonnenschein.prilog.team → :8101 │
│ ├── jugendtreff-nord.prilog.team → :8102 │
│ └── turnverein-linden.prilog.team → :8103 │
│ │
│ PostgreSQL 16 (1 Server, separate DBs) │
│ ├── synapse_kita-sonnenschein │
│ ├── synapse_jugendtreff-nord │
│ └── synapse_turnverein-linden │
│ │
│ MinIO (1 Instanz, separate Buckets) │
│ ├── tenant-kita-sonnenschein │
│ ├── tenant-jugendtreff-nord │
│ └── tenant-turnverein-linden │
│ │
│ Synapse-Container (je ~300-500 MB RAM) │
│ ├── synapse-kita-sonnenschein (:8101) │
│ ├── synapse-jugendtreff-nord (:8102) │
│ └── synapse-turnverein-linden (:8103) │
└─────────────────────────────────────────────────┘Ressourcen pro Shared Host
| Server-Typ | RAM | Max Tenants | Kosten |
|---|---|---|---|
| CX32 | 8 GB | ~15 | ~13 EUR/Monat |
| CX42 | 16 GB | ~25-30 | ~24 EUR/Monat |
Synapse fuer ≤20 User braucht ~200-400 MB RAM. Auf einem CX32 passen bequem 15 Mini-Tenants.
Provisionierung eines Shared-Tenants
Der Agent auf dem Shared Host empfaengt den Befehl shared_tenant.create und fuehrt 7 Schritte aus:
1. PostgreSQL: DB + User anlegen
2. MinIO: Bucket erstellen
3. Synapse: Config + Docker-Compose generieren
4. Synapse: Container starten + Health-Check
5. Nginx: Server-Block mit Wildcard-Cert
6. Port-Registry aktualisieren (/etc/prilog/port-registry.json)
7. Credentials speichernGesamtdauer: ~2-5 Minuten (kein Server-Kauf noetig).
Port-Vergabe
Jeder Tenant bekommt einen eigenen Synapse-Port ab 8101 (sequentiell). Die Port-Registry liegt unter /etc/prilog/port-registry.json:
{
"next_port": 8104,
"tenants": {
"kita-sonnenschein": 8101,
"jugendtreff-nord": 8102,
"turnverein-linden": 8103
}
}Matrix Federation
Federation funktioniert identisch zu Dedicated:
- Remote-Server fragt
GET https://kita-sonnenschein.prilog.team/.well-known/matrix/server - Nginx antwortet mit
{"m.server": "kita-sonnenschein.prilog.team:443"} - Federation-Traffic geht ueber Port 443 an Nginx → weiter an den richtigen Synapse-Port
Was Shared NICHT hat
- Kein n8n (zu ressourcenhungrig fuer Multi-Tenant)
- Kein eigener Hetzner-Server (shared Host)
- Kein eigenes SSL-Zertifikat (Wildcard-Cert fuer alle)
- Kein Volume-basiertes Storage (MinIO-Bucket statt Hetzner-Volume)
Datenbank-Schema
shared_hosts
| Feld | Typ | Beschreibung |
|---|---|---|
| id | SERIAL | Primary Key |
| name | VARCHAR(50) | z.B. "shared-1" |
| ip_address | VARCHAR(45) | Oeffentliche IP |
| tailscale_ip | VARCHAR(45) | Tailscale IP |
| hetzner_type | VARCHAR(50) | z.B. "CX32" |
| max_tenants | INTEGER | Default: 15 |
| status | VARCHAR(20) | active, full, maintenance, offline |
server_orders (neue Felder)
| Feld | Typ | Beschreibung |
|---|---|---|
| hosting_type | VARCHAR(20) | "dedicated" oder "shared" |
| shared_host_id | INTEGER | FK auf shared_hosts (nur bei shared) |
| synapse_port | INTEGER | Port auf dem Shared Host (z.B. 8101) |
| shared_monthly_price | DECIMAL(10,2) | Fixpreis netto (29.00 fuer Starterpaket) |
| max_users_shared | INTEGER | Nutzer-Limit (20 fuer Starterpaket) |
Zahlung & Abrechnung
Prilog Light (Shared) — Stripe
Prilog Light wird vollstaendig ueber Stripe abgerechnet. Es gibt keine Rechnungsoption.
Bestellung
│
▼
Stripe Checkout (Karte hinterlegen)
│
▼
Testphase (10 Tage, voller Zugang, keine Abbuchung)
│
▼ (Tag 10)
Erste Abbuchung: 34,50 EUR inkl. MwSt.
│
├── Erfolgreich → monatlich wiederkehrend
│
└── Fehlgeschlagen → Dunning
│
├── Tag 11: Retry 1
├── Tag 12: Retry 2
└── Tag 13: Retry 3 fehlgeschlagen
│
▼
Account gesperrt (paymentStatus = suspended)
Nutzer sieht Zahlungs-Fenster
│
├── Zahlung nachgeholt → sofort wieder aktiv
│
└── 30 Tage ohne Zahlung → Tenant + Daten geloeschtPayment-Status-Lifecycle:
| Status | paymentHealthStatus | Bedeutung |
|---|---|---|
trial | ok | 10-Tage-Testphase, voller Zugang |
paid | ok | Aktives Abo, Zahlung aktuell |
past_due | overdue | Zahlung fehlgeschlagen, Retries laufen |
suspended | suspended | Account gesperrt, nur Zahlungs-Fenster |
deleted | cancelled | Tenant und Daten geloescht |
Stripe-Events (Webhook POST /api/webhooks/stripe):
| Event | Aktion |
|---|---|
checkout.session.completed | Subscription-ID speichern, Status trial |
invoice.paid | Status → paid, Zugang sicherstellen |
invoice.payment_failed | Status → past_due |
customer.subscription.deleted | Status → suspended, serviceSuspendedAt setzen |
customer.subscription.updated | Status synchronisieren (trial/active/past_due) |
Cron-Jobs:
| Job | Schedule | Aufgabe |
|---|---|---|
stripe-subscription-sync | Taeglich 05:00 | Safety-Net: gleicht Status mit Stripe ab |
suspended-tenant-cleanup | Taeglich 04:30 | Loescht Tenants 30 Tage nach Sperrung |
post-deadline-reminder | Taeglich 08:00 | Frist-Erinnerungen fuer Briefe/Umfragen/Formulare |
absence-attest-reminder | Taeglich 09:00 | Attest-Erinnerung bei Abwesenheiten >= 3 Tage |
reverse-membership-reconciliation | Taeglich 03:00 | Matrix→Prilog Membership-Abgleich |
Self-Service (Web-Client):
Unter Einstellungen → Abrechnung sieht der Nutzer:
- Aktueller Tarif und Preis
- Payment-Status (Testphase / Aktiv / Ausstehend / Gesperrt)
- Testphasen-Enddatum
- Button "Zahlungsmethode verwalten" → oeffnet Stripe Billing Portal
Bei gesperrtem Account wird ein roter Banner angezeigt mit dem Button "Zahlungsmethode aktualisieren".
Prilog Pro (Dedicated) — Rechnung oder Karte
Pro-Kunden waehlen beim Onboarding zwischen Rechnung (Ueberweisung) und Kreditkarte. Die Rechnungsstellung erfolgt manuell ueber das Admin-Portal (Cost-Management-Modul mit Mahn-Workflow).
Cost-Engine
Die Kostenberechnung in cost.service.ts unterscheidet zwei Pfade:
Shared: Stripe verwaltet die Abrechnung. Die manuelle Rechnungserzeugung (generateAllMonthlyInvoices) schliesst Orders mit stripeSubscriptionId automatisch aus.
Dedicated: Wie bisher — Server-Tier-Basispreis + (Nutzer x Preis/Nutzer) + Module + Add-Ons.
Admin-Portal
Shared-Hosts-Verwaltung (/shared-hosts)
- Host-Liste mit Tenant-Count, Status-Badge, IP, Hetzner-Typ
- Aufklappbare Tenant-Tabelle pro Host (Subdomain, Organisation, Port, Nutzer, Status)
- Host-Erstellungsformular
- Status-Aenderung (active → maintenance → offline)
Provisioning
- Dedicated:
POST /admin/orders/:orderId/provision(erstellt Hetzner-Server + Agent) - Shared:
POST /admin/orders/:orderId/provision-shared(weist Shared Host zu, sendet Agent-Command)
API-Endpoints
Onboarding (Public)
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/public/subdomain/check | Subdomain-Verfuegbarkeit pruefen |
| POST | /api/public/orders | Bestellung anlegen (mit hostingType) |
| GET | /api/public/server-tiers | Tier-Liste fuer Dedicated |
Customer Auth
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/customer/login | Login (gibt instances[] bei Multi-Instanz) |
| POST | /api/customer/select-instance | Token fuer gewaehlte Instanz |
Billing (Platform API, authentifiziert)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/platform/v1/billing/status | Tarif, Preis, Status, Trial-Ende |
| POST | /api/platform/v1/billing/portal-session | Stripe Billing Portal URL erstellen |
Admin — Shared Hosts
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/admin/shared-hosts | Alle Shared Hosts mit Tenant-Count |
| POST | /api/admin/shared-hosts | Host anlegen |
| PATCH | /api/admin/shared-hosts/:id | Status/maxTenants aendern |
| GET | /api/admin/shared-hosts/:id/tenants | Tenants auf einem Host |
| POST | /api/admin/orders/:orderId/provision-shared | Shared-Tenant provisionieren |
Settings (Platform API, authentifiziert)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/platform/v1/settings/visibility-matrix | Sichtbarkeits-Matrix laden (mit Auto-Defaults aus UserTypes) |
| PUT | /api/platform/v1/settings/visibility-matrix | Sichtbarkeits-Matrix speichern (nur Admin) |
| GET | /api/platform/v1/post-templates | Formular-/Brief-Vorlagen laden (Auto-Seed beim ersten Aufruf) |
| POST | /api/platform/v1/post-templates | Eigene Vorlage erstellen |
| POST | /api/platform/v1/spaces/:id/posts/:postId/archive | Brief/Umfrage/Formular archivieren (DMS-Dokument + Zusammenfassung) |