Skip to content

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 — StarterpaketDedicated
NutzerBis 20Unbegrenzt
Preis34,50 EUR/Monat inkl. MwSt.Ab 45 EUR Basis + 3 EUR/Nutzer
ServerGeteilter Host (Multi-Tenant)Eigener Hetzner-Server
IsolationDB + Bucket + Synapse-Prozess getrenntKomplett eigener Server
n8nNicht verfuegbarEigene n8n-Instanz
Provisionierung~2-5 Minuten~15-30 Minuten
SubdomainFreitext, z.B. turnverein-linden.prilog.teamFreitext, z.B. weser.prilog.team
ZielgruppeVereine, Kitas, kleine EinrichtungenSchulen, 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 Zugangsdaten

Subdomain-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 → Dashboard

Technische Umsetzung

  • POST /api/customer/login: Prueft alle PortalAccount-Eintraege fuer die E-Mail
  • Bei >1 Match: Antwort mit multiInstance: true und instances[] (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-TypRAMMax TenantsKosten
CX328 GB~15~13 EUR/Monat
CX4216 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 speichern

Gesamtdauer: ~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:

json
{
  "next_port": 8104,
  "tenants": {
    "kita-sonnenschein": 8101,
    "jugendtreff-nord": 8102,
    "turnverein-linden": 8103
  }
}

Matrix Federation

Federation funktioniert identisch zu Dedicated:

  1. Remote-Server fragt GET https://kita-sonnenschein.prilog.team/.well-known/matrix/server
  2. Nginx antwortet mit {"m.server": "kita-sonnenschein.prilog.team:443"}
  3. 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

FeldTypBeschreibung
idSERIALPrimary Key
nameVARCHAR(50)z.B. "shared-1"
ip_addressVARCHAR(45)Oeffentliche IP
tailscale_ipVARCHAR(45)Tailscale IP
hetzner_typeVARCHAR(50)z.B. "CX32"
max_tenantsINTEGERDefault: 15
statusVARCHAR(20)active, full, maintenance, offline

server_orders (neue Felder)

FeldTypBeschreibung
hosting_typeVARCHAR(20)"dedicated" oder "shared"
shared_host_idINTEGERFK auf shared_hosts (nur bei shared)
synapse_portINTEGERPort auf dem Shared Host (z.B. 8101)
shared_monthly_priceDECIMAL(10,2)Fixpreis netto (29.00 fuer Starterpaket)
max_users_sharedINTEGERNutzer-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 geloescht

Payment-Status-Lifecycle:

StatuspaymentHealthStatusBedeutung
trialok10-Tage-Testphase, voller Zugang
paidokAktives Abo, Zahlung aktuell
past_dueoverdueZahlung fehlgeschlagen, Retries laufen
suspendedsuspendedAccount gesperrt, nur Zahlungs-Fenster
deletedcancelledTenant und Daten geloescht

Stripe-Events (Webhook POST /api/webhooks/stripe):

EventAktion
checkout.session.completedSubscription-ID speichern, Status trial
invoice.paidStatus → paid, Zugang sicherstellen
invoice.payment_failedStatus → past_due
customer.subscription.deletedStatus → suspended, serviceSuspendedAt setzen
customer.subscription.updatedStatus synchronisieren (trial/active/past_due)

Cron-Jobs:

JobScheduleAufgabe
stripe-subscription-syncTaeglich 05:00Safety-Net: gleicht Status mit Stripe ab
suspended-tenant-cleanupTaeglich 04:30Loescht Tenants 30 Tage nach Sperrung
post-deadline-reminderTaeglich 08:00Frist-Erinnerungen fuer Briefe/Umfragen/Formulare
absence-attest-reminderTaeglich 09:00Attest-Erinnerung bei Abwesenheiten >= 3 Tage
reverse-membership-reconciliationTaeglich 03:00Matrix→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)

MethodePfadBeschreibung
POST/api/public/subdomain/checkSubdomain-Verfuegbarkeit pruefen
POST/api/public/ordersBestellung anlegen (mit hostingType)
GET/api/public/server-tiersTier-Liste fuer Dedicated

Customer Auth

MethodePfadBeschreibung
POST/api/customer/loginLogin (gibt instances[] bei Multi-Instanz)
POST/api/customer/select-instanceToken fuer gewaehlte Instanz

Billing (Platform API, authentifiziert)

MethodePfadBeschreibung
GET/api/platform/v1/billing/statusTarif, Preis, Status, Trial-Ende
POST/api/platform/v1/billing/portal-sessionStripe Billing Portal URL erstellen

Admin — Shared Hosts

MethodePfadBeschreibung
GET/api/admin/shared-hostsAlle Shared Hosts mit Tenant-Count
POST/api/admin/shared-hostsHost anlegen
PATCH/api/admin/shared-hosts/:idStatus/maxTenants aendern
GET/api/admin/shared-hosts/:id/tenantsTenants auf einem Host
POST/api/admin/orders/:orderId/provision-sharedShared-Tenant provisionieren

Settings (Platform API, authentifiziert)

MethodePfadBeschreibung
GET/api/platform/v1/settings/visibility-matrixSichtbarkeits-Matrix laden (mit Auto-Defaults aus UserTypes)
PUT/api/platform/v1/settings/visibility-matrixSichtbarkeits-Matrix speichern (nur Admin)
GET/api/platform/v1/post-templatesFormular-/Brief-Vorlagen laden (Auto-Seed beim ersten Aufruf)
POST/api/platform/v1/post-templatesEigene Vorlage erstellen
POST/api/platform/v1/spaces/:id/posts/:postId/archiveBrief/Umfrage/Formular archivieren (DMS-Dokument + Zusammenfassung)