Skip to content

Freemium mit 90-Tage-Hide + 12-Monats-Wipe — Konzept

Stand: 2026-04-30 (Revision 2: Hide statt Delete) Modell: 90 Tage gratis testen, danach werden Inhalte aelter 90 Tage ausgeblendet (nicht geloescht). Mit Abo (3 EUR/aktivem User/Monat) sofort wieder voll sichtbar. Hard-Delete nur bei explizitem Konto-Schluss oder bei 12+ Monaten Inaktivitaet ohne Abo.


1. Drei-Stufen-Modell

StufeTriggerWirkung
SichtbarDefault (Abo aktiv ODER alles juenger 90d)Alle Daten zugaenglich
HiddenOhne Abo, Inhalte aelter 90dPer Query-Filter ausgeblendet. Banner: "X Tage alte Inhalte ausgeblendet — Abo aktiviert sie wieder."
Deleted(a) Admin klickt "Konto schliessen" → 30d Gnadenfrist + Hard-Delete
(b) Tenant 12+ Monate kein Login UND kein Abo → Auto-Cancel-Mail + 30d + Wipe
DSGVO-konformer Hard-Delete inkl. S3 + Synapse

Vorteile:

  • Risiko-arm: keine versehentliche Loeschung. Bug im Filter? Daten sind noch da.
  • Conversion-stark: "Klick → alles wieder da" ist staerker als "zahl, sonst weg".
  • Reaktivierung trivial: Pause-Tenant kommt zurueck → Abo → weiter wie vorher.
  • Implementation: ~4 Tage statt ~8 Tage (kein S3-Cleanup-Cron, kein Synapse-Per-Room-Retention).

2. Tabellen-Inventur

Drei Kategorien (Compliance + Platform-Global aus Inventur Rev. 1 unveraendert).

A) Hide-Filter pflichtig (User-Content)

Diese Tabellen bekommen einen WHERE-Filter im Listen-/Detail-Service. Nicht alle Tabellen brauchen ein Filter — Strukturobjekte (Boards, UserTypes etc.) bleiben immer sichtbar, nur Items darin werden gefiltert.

Tabelle / ServiceHide-Filter aufBemerkung
Document (DMS-Liste, Suche)updatedAtinkl. Volltextsuche
WorkItem (Tasks-Liste)updatedAtoffene Tasks bleiben durch updatedAt
WorkItemCommentfiltert mit Parent
CalendarEventupdatedAt ODER endsAtRecurring Events: recurrenceUntil
AbsenceEntryupdatedAt
MitteilungsheftEntryupdatedAt
SpacePost + ResponsesupdatedAt
MorningCheck + EntrycreatedAtevent-typisch, kein Update
CollabDocumentupdatedAtY.js wird oft geupdated, daher harmlos
GeneratedReportcreatedAt
DistributioncreatedAt
InboxDropMetacreatedAt
ProcessInstance + EventsupdatedAt UND status='completed'laufende Flows immer sichtbar
CrisisReportcreatedAt
SchoolTripEventendsAt
SpaceActivityDaydateHeatmap-Datenpunkt
Tag / DocumentTag / ContactTagfiltert mit Parent
FavoritecreatedAt
SpaceEmail + InboundEmail + RepliescreatedAt
MessageReadReceiptfiltert mit Message
Matrix-Messages (Synapse)origin_server_tsClient-seitiger Timeline-Filter (siehe §4.3)

B) Strukturell — kein Filter (sonst kollabiert der Tenant)

Tenant, User, UserDirectoryEntry, Membership, Space, SpaceManager, UserType, TenantSetting, ApiKey, TenantModuleInstallation, ServerOrder, Plan, Addon, RoleDefault, RolePermissionPolicy, BoardGroup, Board, FamilyRelation, Templates (SpaceTemplate, ConceptTemplate, ReportTemplate, SpacePostTemplate), ProcessTemplate/Component/Edge, AccessRequest, UserInvitation, PortalAccount, DeveloperAccount, SavedFilter, UserPostfachSettings, UserOnboarding, LayerSubscription, CalendarLayer.

C) Compliance — exempt (10-Jahres- bis dauerhaft-Aufbewahrung)

ChildProtectionCase, Invoice, InvoiceItem, AvvConsentLog, EmailLog, ImpersonationLog, ImpersonationNotice, PostfachAuditLog, CrisisAuditEntry, UserDirectoryHistory, ProvisionStep/Script, InstallationLog, Credential, MaintenanceSnapshot, Ticket/Reply, SyncJob/Event, HealEvent, SmokeTestRun.


3. Schema-Aenderungen

prisma
model Tenant {
  ...bestehend...
  subscriptionStatus    String   @default("trial")   // 'trial' | 'active' | 'cancelled'
  subscriptionStartedAt DateTime?
  subscriptionEndedAt   DateTime?
  creditCents           Int      @default(0)
  stripeCustomerId      String?
  stripeSubscriptionId  String?
  // Fuer 12-Monats-Zombie-Detection:
  lastActivityAt        DateTime?  // letzter Login eines Users
  scheduledDeletionAt   DateTime?  // wenn gesetzt, Hard-Delete an dem Datum
}

model UserActivity {
  id          String   @id @default(uuid())
  tenantId    String
  userId      String
  periodMonth String   // 'YYYY-MM'
  loginCount  Int      @default(0)
  writeCount  Int      @default(0)
  lastSeenAt  DateTime
  @@unique([userId, periodMonth])
  @@index([tenantId, periodMonth])
}

4. Implementierung

4.1 Hide-Filter (Hauptarbeit)

Ein zentraler Helper:

ts
// services/data-visibility.service.ts
export async function getVisibilityCutoff(tenantId: string): Promise<Date | null> {
  const tenant = await prisma.tenant.findUnique({ where: { id: tenantId } });
  if (tenant?.subscriptionStatus === 'active') return null; // alles sichtbar
  return new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
}

Pro Service der Liste A:

ts
const cutoff = await getVisibilityCutoff(tenantId);
const where = {
  tenantId,
  ...(cutoff ? { updatedAt: { gte: cutoff } } : {}),
};

Geschaetzt: 20-25 Service-Files anzupassen.

4.2 Active-User-Tracking

  • Login-Hook: Bei /auth/login-Erfolg Upsert UserActivity { loginCount++ }
  • Write-Hook: Bei Message-Send, Document-Create, WorkItem-Create/Update, CalendarEvent-Create, Comment-Create Upsert writeCount++
  • Aktiv: loginCount > 0 AND writeCount > 0 im Period-Monat
  • Tenant.lastActivityAt wird bei jedem Login gesetzt (fuer Zombie-Detection)

4.3 Matrix-Timeline-Filter

Synapse selbst loescht/blendet nichts aus. Aber: der Web-Client laedt Messages via /sync und stellt sie selbst dar. Wir filtern client-seitig in der Render-Pipeline:

ts
// chat-store.ts
const cutoffTs = sessionStore.getSnapshot().subscription?.visibilityCutoff;
const visibleMessages = messages.filter(m => !cutoffTs || m.timestamp >= cutoffTs);

Das visibilityCutoff kommt aus dem Bootstrap-Endpoint. Bei aktivem Abo: null → alles sichtbar.

4.4 Banner + Status-Endpoint

  • GET /platform/v1/subscription/status liefert:
    json
    { "status": "trial", "trialDaysLeft": 12, "hiddenSince": "2026-01-30", "creditCents": 600 }
  • Banner-Logik:
    • status === 'trial' && trialDaysLeft > 7: kein Banner
    • status === 'trial' && trialDaysLeft <= 7: gelber Banner "X Tage bis erste Inhalte ausgeblendet werden"
    • status === 'trial' && hiddenSince: roter Banner "Inhalte aelter X Tage sind ausgeblendet — [Jetzt 3 EUR/User abonnieren]"

4.5 Stripe-Integration

  • Checkout: Stripe-Checkout-Session mit Subscription-Mode (3 EUR/User-Preis konfiguriert in Stripe-Dashboard)
  • Webhook: customer.subscription.createdsubscriptionStatus = 'active'. subscription.deleted'cancelled'.
  • Billing-Cron (monatlich, 1. um 06:00 UTC):
    Pro Tenant mit subscriptionStatus='active':
      activeUsers = count(UserActivity wo periodMonth=lastMonth UND loginCount>0 UND writeCount>0)
      grossCents = activeUsers * 300
      netCents = max(0, grossCents - tenant.creditCents)
      if netCents > 0: stripe.invoiceItems.create + stripe.invoices.finalize
      inactiveUsers = membership.count - activeUsers
      creditCents += inactiveUsers * 300
      Cap: creditCents = min(creditCents, 6 * membership.count * 300)

4.6 Hard-Delete-Pfade

Pfad 1 — Admin schliesst Konto:

  • Settings → Rechnungen → "Konto schliessen" → Bestaetigungs-Modal mit "Daten werden in 30 Tagen unwiderruflich geloescht"
  • Tenant.scheduledDeletionAt = now() + 30d
  • Banner: "Dein Konto wird am DD.MM.YYYY geloescht. [Abbrechen]"
  • 30d-Cron prueft scheduledDeletionAt < now() → Hard-Wipe

Pfad 2 — 12-Monats-Zombie:

  • Cron taeglich: Tenants mit subscriptionStatus != 'active' UND lastActivityAt < now() - 365d UND scheduledDeletionAt IS NULL
  • Setze scheduledDeletionAt = now() + 30d, sende Mail an Admin: "Inaktivitaet seit 12 Monaten — Konto wird geloescht. [Reaktivieren-Link]"

Hard-Wipe-Job:

  • Loescht Tenant + alle abhaengigen Tabellen (cascade)
  • Loescht S3-Bucket fuer den Tenant
  • Loescht Synapse-Raeume via Admin-API
  • Schreibt AvvConsentLog-Eintrag (DSGVO-Loesch-Beleg, exempt von der Wipe selbst)

4.7 Onboarding-Update

  • Signup-Page: "90 Tage gratis. Keine Karte noetig. Danach 3 EUR pro aktivem User/Monat."
  • Welcome-Modal beim ersten Login: "Inhalte aelter 90 Tage werden ohne Abo ausgeblendet — nie geloescht. Click zum Abo bringt sie zurueck."

5. Phasenplan

#PhaseAufwand
1Schema-Migration (Tenant + UserActivity)0.5d
2Active-User-Tracking (Login + Write Hooks)1d
3Hide-Filter in 20-25 Services + Matrix-Timeline-Filter im Web-Client1.5d
4Banner + Subscription-Status-Endpoint0.5d
5Stripe-Integration (Checkout + Webhook + Billing-Cron)2d
6Onboarding-Update + Welcome-Modal0.5d
7Hard-Delete-Pfade (Admin-Schliessung + 12M-Zombie)1d
8E2E-Test + Doku0.5d

Gesamt: ~7.5 Tage (vs. 8d in Rev. 1 — kaum gespart, aber Risiko deutlich kleiner und Conversion staerker)


6. Offene Fragen (vor Phase 1)

  1. Filter-Feld: updatedAt als Default — OK?
  2. Aktiv-Definition: loginCount > 0 AND writeCount > 0 im Monat — OK?
  3. Credit-Cap: 6 Monate (entspricht halbjaehrlicher Vorrat) — OK?
  4. Trial-Verlaengerung: Soll Admin Trial selbst um +30d schieben koennen oder nur Vollabo?
  5. Member-Count fuer Credit: alle PrilogMemberships oder nur "echte" User (keine Bots)?