11. September 2026
Deine App schaltet sich selbst frei
Der Kauf-Dialog meldet Erfolg, die App setzt ein Flag, Pro ist offen. Damit entscheidet der Client, wer bezahlt hat. In der Browser-Konsole dauert das eine Zeile.
Der Checkout ist zurück, status: 'success', die App setzt isPro = true und schaltet frei. Der Nutzer sieht sofort, was er gekauft hat. Kein Laden, kein Warten, keine zweite Anfrage.
Genau daran hängt das Problem. Die App hat gerade selbst entschieden, dass jemand bezahlt hat.
Im Post davor ging es darum, wer die Nachricht geschickt hat. Hier geht es eine Ebene höher: wer entscheidet, was der Nutzer darf.
Was die KI dir gebaut hat
Bei einem Stripe-Abo baut Lovable inzwischen von selbst eine Server-Funktion mit Webhook. Bei einer einmaligen Freischaltung, in einer nativen App oder wenn du im Prompt einfach nur nach dem Flag fragst, kommt weiter das hier:
// nach der Rückkehr aus dem Checkout
if (result.status === 'success') {
localStorage.setItem('isPro', 'true');
setIsPro(true);
}
// beim Start
const isPro = localStorage.getItem('isPro') === 'true';
Das läuft. Der Testkauf schaltet frei, der Reload behält es, das Feature ist fertig.
Der ganze Beweis für die Zahlung steht im Browser des Nutzers. In einer nativen App heißt die Stelle SharedPreferences oder UserDefaults, das Muster ist dasselbe.
Der Test dauert zehn Sekunden
Browser-Konsole auf, eine Zeile:
localStorage.setItem('isPro', 'true')
Neu laden. Wenn Pro jetzt offen ist, weißt du Bescheid. Kein Werkzeug, kein Angriff, keine Anmeldung. Nur die Konsole, die in jedem Browser eingebaut ist.
Die Variante mit dem State ohne Speicher ist keinen Deut besser. Da öffnest du die React DevTools und setzt den Wert direkt.
Die zweite Fassung: der Client schreibt in die Datenbank
Wenn ein Backend im Spiel ist, kommt oft diese Version:
await supabase.from('profiles')
.update({ is_pro: true })
.eq('id', user.id);
Das sieht serverseitig aus, weil eine Datenbank dranhängt. Es ist trotzdem der Client, der schreibt. Wer den öffentlichen Schlüssel aus dem Bundle nimmt, schickt dieselbe Anfrage von Hand. Die Spalte is_pro gehört dem Server. Der Client liest sie und schreibt sie nie.
Was stattdessen dahingehört
Der Server beantwortet eine Frage: Was darf dieser Nutzer? Die Grundlage ist das, was der Zahlungsanbieter bestätigt hat, eingetragen über den geprüften Webhook.
// Server entscheidet
app.get('/api/entitlements', requireAuth, async (req, res) => {
const sub = await db.getSubscription(req.user.id); // aus dem Webhook
const aktiv = sub?.status === 'active' && sub.periodEnd > Date.now();
res.json({ pro: aktiv, gueltigBis: sub?.periodEnd ?? null });
});
// Client fragt
const { pro, gueltigBis } = await fetch('/api/entitlements')
.then(r => r.json());
// Rückkehr aus dem Checkout: neu fragen, nichts setzen
onCheckoutReturn(() => refetchEntitlements());
Der Unterschied steht in der letzten Zeile. Der Kauf-Callback löst eine neue Abfrage aus. Er ist ein Signal, kein Beweis. Er darf auch ausbleiben, wenn der Nutzer den Tab schließt, und der Server weiß trotzdem Bescheid.
Ein Punkt, den man dabei leicht überliest: Die Antwort steuert die Oberfläche. Sie ersetzt die Prüfung an der Funktion nicht. Wenn pro: false nur den Export-Button ausgraut, der Endpunkt /api/export aber jeden bedient, hast du die Entscheidung wieder im Client. Jede bezahlte Funktion prüft die Berechtigung selbst, an der Stelle, an der sie ausgeführt wird.
Die Stelle, an der es auch dann noch klemmt
Niemand fragt bei jedem Klick beim Server nach. Also cachest du die Antwort. Und damit hast du wieder einen lokalen Wert, der Zugang gewährt, nur diesmal einen, der mal richtig war.
Drei Dinge, die dabei geklärt sein müssen:
Wie lange gilt der Cache. Die Cache-Dauer ist das Zeitfenster, in dem ein gekündigter oder zurückerstatteter Nutzer weiter Pro hat. Minuten sind in Ordnung. Eine Woche ist eine Woche Gratis-Zugang nach der Kündigung. Bei jedem Kaltstart und beim Zurückkehren in den Vordergrund neu fragen.
Was bei Ablauf passiert. Der Server liefert gueltigBis mit. Ist der Zeitpunkt vorbei und die App bekommt keine frische Antwort, fällt sie auf den freien Stand zurück. Ohne diesen Ablauf ist der Cache dauerhaft und du bist beim localStorage-Flag vom Anfang.
Was offline gilt. Ein zahlender Nutzer im Zug darf seine App benutzen. Der letzte bekannte Status gilt bis gueltigBis, danach ist Schluss. Bei jährlichen Abos deckelst du das zusätzlich, sonst läuft eine App elf Monate ohne einen einzigen Serverkontakt weiter.
Der Cache ist eine Bequemlichkeit. Die Quelle bleibt der Server.
Wo das gesammelt liegt
Webhook-Signatur, Entitlement-Endpunkt, Prüfung an der Funktion, Cache-Dauer: Das hängt zusammen und keine dieser Stellen meldet sich von selbst, wenn sie fehlt. Dazu kommt der Teil, der erst in Produktion auftaucht. Was passiert bei einer Rückerstattung mitten in der Periode. Wie Trial und erste Abbuchung auseinanderlaufen. Wann ein Abo als gekündigt gilt und ab wann es zumacht.
Der Payment-Guide stellt das geordnet zusammen. Die Grundlagen stehen in der Anbieter-Doku. Hier geht es um die Stellen, an denen man beim ersten Mal danebengreift.
Heute reicht die Konsole. Setz dir dein eigenes Flag und schau, was danach offen ist.
Diplom-Informatiker · 25 Jahre Entwicklung · LinkedIn
PDF plus Beispiele · 197 € · framework-unabhängig
Payment & Web-Checkout Best PracticeWeitere Artikel
4. September 2026
Du versteckst den falschen Schlüssel
Der öffentliche Supabase-Schlüssel gehört ins Frontend, der ist mit Absicht öffentlich. Der geheime umgeht jede Policy. Vier Suchbefehle, die zeigen, ob er irgendwo gelandet ist, wo er nicht hingehört.
28. August 2026
Prüf durch Ausführen, nicht durch Lesen
Lesen sagt dir, was du glaubst, dass passiert. Ausführen sagt dir, was passiert. Warum eine kleine Abfrage zehn gelesene Dateien schlägt — und wie du eine KI-Behauptung in dreißig Sekunden gegen die Realität testest.