Purchase-Flow Analyse
Überblick
Der aktuelle Boost-Purchase-Flow nutzt Stripe Checkout Sessions. Dies ist ein solider Standard-Ansatz, hat aber mehrere Friction Points die zur niedrigen Conversion führen. Dieser Dokument analysiert die aktuelle Implementation und schlägt konkrete Optimierungen vor.
Aktueller Flow: Stripe Checkout
graph LR
A["User öffnet
Song Card"] -->|Sieht Boost Button| B["Klickt
Boost Button"]
B -->|Frontend Call| C["createCheckoutSession
Netlify Function"]
C -->|Erstellt Session| D["Stripe Checkout
URL"]
D -->|Redirect| E["User verlässt
die App"]
E -->|Zahlt bei Stripe| F["Stripe Process
Payment"]
F -->|Webhook POST| G["stripeWebhook
Netlify Function"]
G -->|Update DB| H["Queue Position
Update"]
H -->|Redirect Back| I["Supabase
Realtime Update"]
I -->|User sieht
Position Change| J["Song Boost
Erfolgreich"]Code Structure (vermutlich)
Frontend (app/web/src/components/viewer/):
// BoostButton.jsx
const handleBoost = async () => {
const { data } = await fetch('/.netlify/functions/createCheckoutSession', {
method: 'POST',
body: JSON.stringify({ songId, userId, amount: 5.00 })
});
// Redirect to Stripe
window.location.href = data.url;
}
Backend (Netlify Function):
// functions/createCheckoutSession.js
export const handler = async (event) => {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price: 'price_XXX',
quantity: 1,
}],
mode: 'payment',
success_url: 'https://breakout.music/session?boost=success',
cancel_url: 'https://breakout.music/session',
});
return { statusCode: 200, body: JSON.stringify({ url: session.url }) };
}
// functions/stripeWebhook.js
export const handler = async (event) => {
const sig = event.headers['stripe-signature'];
const stripeEvent = stripe.webhooks.constructEvent(
event.body,
sig,
STRIPE_WEBHOOK_SECRET
);
if (stripeEvent.type === 'checkout.session.completed') {
const { metadata: { songId, userId } } = stripeEvent.data.object;
// Update queue position in PostgreSQL
await supabase.from('song_boosts').insert({
song_id: songId,
user_id: userId,
amount: 5.00,
created_at: new Date()
});
// Trigger Realtime Update
supabase.channel(`song:${songId}`).send({
type: 'broadcast',
event: 'boost_added',
payload: { boost_count: newTotal }
});
}
}
Friction Points Analyse
🔴 Kritische Friction Points
1. App-Verlassen = Conversion Killer
Problem: User klickt Boost → wird zu Stripe redirected → verlässt App → möchte nicht zurückkommen
Warum schlecht:
- Stripe Checkout ist nicht optimiert für Music App Experience
- Mobile: User können nicht einfach zurückkommen
- Phishing-Risiken wenn URL nicht perfekt ist
- Session-State geht verloren
Conversion Impact: -30% bis -50% (Industry Standard: Embedded Payment = 20-30% höher Conversion)
Lösung: Stripe Elements für In-App Payment
2. Keine "One-Click" Repeat Purchases
Problem: User boosted einmal, nächstes Mal muss er wieder Stripe-Formular ausfüllen
Warum schlecht:
- Stripe speichert Payment Method, aber Breakout nutzt sie nicht
- Jede Boost braucht neue Transaktion + Redirect
- Friction für "Power Users" die mehrmals boosten wollen
Conversion Impact: -20% für Repeat Purchases
Lösung: Save Payment Method, Show Saved Cards, "1-Click Boost" Button
3. Keine Preis-Verankerung oder Urgency
Problem: User sieht einfach "Boost" Button ohne Context
Warum schlecht:
- Keine Indication wie teuer es ist
- Keine Urgency warum jetzt boosten (nicht später)
- Keine Social Proof (andere Leute boosten auch)
- Kein Vergleich zwischen Boost-Größen
Conversion Impact: -25% (Menschen treffen Entscheidungen besser mit Context)
Lösung: Pre-Checkout Screen mit Pricing, Urgency, Social Proof
4. Boost ist "Optional" - Nicht Core
Problem: Boost Button ist vielleicht unauffällig neben Vote Button
Warum schlecht:
- Menschen verstehen nicht dass Boost das Priority System verändert
- Vote vs Boost sind nicht klar unterschieden
Conversion Impact: -40% (zu unauffällig)
Lösung: Boost Button als Primary, Vote als Secondary. "Boost = Pay to Move Up Queue"
5. Keine Positional Preview
Problem: User zahlt 5€ und weiß nicht ob Song von Position 47 zu Position 32 wechselt
Warum schlecht:
- Ist die 5€ wert? Weiß ich nicht!
- Keine Accountability für den Boost-Effekt
- User könnte merken dass Boost keine Effekt hat und beschweren
Conversion Impact: -15% (Unsicherheit reduziert Purchase Intent)
Lösung: Vor Checkout: "Boost wird Song von #47 zu #32 bewegen (Rechner zeigen)"
6. Queue-Funktion unklar
Problem: Neue User verstehen nicht dass man "Boosts" kaufen kann um nach oben zu kommen
Warum schlecht:
- Vielleicht denken sie Votes sind alles
- Boost-Funktion wird nie entdeckt
Conversion Impact: -50% (Feature wird einfach nicht genutzt)
Lösung: Onboarding erklärt Boost-System, Queue-Legende sichtbar
🟡 Mittlere Friction Points
7. Keine Bundle Pricing
Problem: Nur Single-Boost-Preis verfügbar (z.B. €5)
Warum suboptimal:
- Keine Bulk Discount Incentive
- Impulse-Buys nicht gefördert
- Revenue-pro-User niedrig
Lösung: Tiered Pricing
- Small Boost: €2 (1 Position Up)
- Medium Boost: €5 (3 Positions Up)
- Large Boost: €10 (5 Positions Up)
- Bundle: 5 Boosts für €20 (statt €25)
8. Keine Dynamic Pricing
Problem: €5 Boost ist immer €5, egal wie viele andere gerade boosten
Warum suboptimal:
- Wenn 1000 User gleichzeitig boosten, sollte Preis höher sein (Scarcity)
- Wenn wenig Booste, sollte Preis niedriger sein (Encouragement)
- Revenue wird nicht maximiert
Lösung: Dynamic Pricing basierend auf Queue-Größe
Queue >= 100 Boosts in letzten 24h → Preise +20%
Queue <= 10 Boosts → Preise -10%
9. Keine Social Proof im Checkout
Problem: User sieht Stripe Form, keine Indication dass viele andere das auch machen
Warum suboptimal:
- Stripe hat "Trust Badges" aber Breakout Context ist verloren
- FOMO-Effekt ist nicht vorhanden
- Social Validation ist psychologisch wichtig
Lösung: Pre-Checkout Screen zeigt "200 people boosted in this session"
10. Keine Urgency Signals
Problem: "Boost jederzeit" - keine Deadline
Warum suboptimal:
- Menschen sind faul und procrastinate
- Keine Motivation JETZT zu boosten (nicht später)
- Conversion-Rate flach
Lösung: "Queue closes in 15 minutes" oder "Last 5 songs in queue" messaging
🟢 Minor Friction Points
11. Keine Error Recovery
Problem: Wenn User Checkout abbricht, zurück zur App und... was jetzt?
Lösung: "You almost completed your boost! Try again?" Message
12. Keine Mobile Optimization
Problem: Stripe Checkout kann auf Mobile suboptimal sein
Lösung: Test auf iPhone/Android, eventuell Stripe Mobile Optimized Checkout
Verbesserte Flow: Stripe Elements + In-App Checkout
graph LR
A["User öffnet
Song Card"] -->|Sieht Boost Button
mit Preis| B["Klickt Boost
Button"]
B -->|Modal öffnet| C["Pre-Checkout
Screen"]
C -->|Zeigt:
Position Preview
Price Anchoring
Social Proof
Urgency| D["User sieht
Boost von #47→#32
€5"]
D -->|Weiter| E["Stripe Elements
Card Form"]
E -->|In-App Payment
Keine Redirect| F["Processing..."]
F -->|Webhook Success| G["Realtime Update
via Supabase"]
G -->|Konfetti +
Success Message| H["Boost Complete
User bleibt in App"]
H -->|Optional: Save
Payment Method| I["Nächstes Mal:
1-Click Boost"]Implementation Details
Frontend Komponente (neuer Flow):
// BoostModal.jsx
export const BoostModal = ({ song, currentPosition, onClose }) => {
const [selectedTier, setSelectedTier] = useState('medium');
const [cardElement, setCardElement] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
// Calculate Position Preview
const positionDelta = {
small: -1,
medium: -3,
large: -5
};
const newPosition = Math.max(1, currentPosition + positionDelta[selectedTier]);
const handleSubmit = async (e) => {
e.preventDefault();
setIsProcessing(true);
try {
// 1. Create Payment Intent (not Session)
const { clientSecret } = await fetch('/.netlify/functions/createPaymentIntent', {
method: 'POST',
body: JSON.stringify({
songId: song.id,
userId: user.id,
amount: PRICING[selectedTier],
savePaymentMethod: true
})
}).then(r => r.json());
// 2. Confirm Payment with Card Element (no redirect!)
const { paymentIntent, error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: user.name, email: user.email }
}
});
if (error) throw new Error(error.message);
// 3. Success! Show Celebration
showConfetti(); // celebratory animation
// 4. Realtime subscription handles queue update
// Webhook telah trigger, Supabase akan broadcast
setTimeout(() => onClose(), 2000);
} catch (err) {
showError(err.message);
} finally {
setIsProcessing(false);
}
};
return (
<Modal isOpen={true} onClose={onClose}>
<h2>Boost "{song.title}"</h2>
{/* Social Proof */}
<div className="bg-green-50 p-3 rounded mb-4">
✨ 247 people boosted songs in this session today
</div>
{/* Urgency Signal */}
<div className="bg-yellow-50 p-3 rounded mb-4">
⏰ Queue closes in 12 minutes
</div>
{/* Pricing Tiers */}
<div className="grid grid-cols-3 gap-2 mb-4">
{['small', 'medium', 'large'].map(tier => (
<button
key={tier}
onClick={() => setSelectedTier(tier)}
className={`p-3 border-2 rounded ${
selectedTier === tier ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
}`}
>
<div className="font-bold">{PRICING[tier]}€</div>
<div className="text-xs">+{positionDelta[tier]} Pos</div>
</button>
))}
</div>
{/* Position Preview */}
<div className="mb-4">
<div className="text-sm text-gray-600">Your impact:</div>
<div className="text-lg font-bold">
#{currentPosition} → #{newPosition}
</div>
</div>
{/* Payment Form */}
<form onSubmit={handleSubmit}>
<CardElement
onChange={(e) => setCardElement(e.complete ? e.element : null)}
className="mb-4"
/>
<label className="flex items-center gap-2 mb-4">
<input type="checkbox" defaultChecked />
<span className="text-sm">Save for next time (faster checkout)</span>
</label>
<button
type="submit"
disabled={isProcessing || !cardElement}
className="w-full bg-blue-500 text-white py-2 rounded font-bold disabled:bg-gray-300"
>
{isProcessing ? 'Processing...' : `Boost for €${PRICING[selectedTier]}`}
</button>
</form>
</Modal>
);
};
Backend (neue Payment Intent Function):
// functions/createPaymentIntent.js
export const handler = async (event) => {
const { songId, userId, amount, savePaymentMethod } = JSON.parse(event.body);
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Convert to cents
currency: 'eur',
payment_method_types: ['card'],
metadata: {
songId,
userId,
type: 'boost'
},
// Save payment method for one-click checkout
setup_future_usage: savePaymentMethod ? 'on_session' : undefined
});
return {
statusCode: 200,
body: JSON.stringify({ clientSecret: paymentIntent.client_secret })
};
};
Webhook bleibt ähnlich, aber triggered durch confirmCardPayment statt Session:
// functions/stripeWebhook.js
export const handler = async (event) => {
const sig = event.headers['stripe-signature'];
const stripeEvent = stripe.webhooks.constructEvent(
event.body,
sig,
STRIPE_WEBHOOK_SECRET
);
if (stripeEvent.type === 'payment_intent.succeeded') {
const { metadata: { songId, userId } } = stripeEvent.data.object;
// Record boost
await supabase.from('song_boosts').insert({
song_id: songId,
user_id: userId,
amount: stripeEvent.data.object.amount / 100,
payment_intent_id: stripeEvent.data.object.id,
created_at: new Date()
});
// Realtime broadcast to all listeners
supabase.channel(`song:${songId}`).send({
type: 'broadcast',
event: 'boost_added',
payload: {
boost_count: newTotal,
newPosition: calculateNewPosition(songId)
}
});
}
};
Revenue Optimierungs-Strategien
1. A/B Testing Preise
Hypothese 1: Tiered Pricing konvertiert besser als Single Price
Variant A (Control): €5 Boost only
Variant B (Test): €2 / €5 / €10 Tiers
Metrik: Conversion Rate (% of viewers who boost)
Erwartet: +15-25% höher
Hypothese 2: Lower price floor konvertiert mehr
Variant A: €2 / €5 / €10
Variant B: €1 / €3 / €7
Metrik: Revenue per viewer (auch wenn Conversion höher, kann ARPU sinken)
Erwartet: Variant A better
2. Dynamic Pricing nach Queue-Volumen
// calculateBoostPrice.js
export const getBoostPrice = async (sessionId) => {
const boostCount = await countBoostsLast24h(sessionId);
if (boostCount >= 100) return { small: 2.50, med: 6.25, large: 12.50 }; // +25%
if (boostCount >= 50) return { small: 2.20, med: 5.50, large: 11.00 }; // +10%
if (boostCount >= 20) return { small: 2.00, med: 5.00, large: 10.00 }; // baseline
return { small: 1.80, med: 4.50, large: 9.00 }; // -10% (encouragement)
};
3. Bundle Discounts
Buy 1 Boost: €5
Buy 3 Boosts: €14 (save €1)
Buy 5 Boosts: €20 (save €5) ← Sweetspot
Psychologischer Effekt: "Save €5" ist motivierender als "20% discount"
4. Urgency & FOMO
// In BoostModal
const timeUntilQueueClose = calculateTimeUntilQueueClose(session);
if (timeUntilQueueClose < 15 * 60 * 1000) { // < 15 minutes
return <UrgencyBadge text={`Queue closes in ${minutes}m`} />;
}
if (songsInQueue < 10) {
return <UrgencyBadge text="Last 10 songs - limited spots!" />;
}
5. Gamification: "Boost Streaks"
Wenn User 3 Sessions in a row boosted → nächster Boost -20%
const userBoostStreak = await getConsecutiveSessionsWithBoost(userId);
if (userBoostStreak >= 3) {
discountMultiplier = 0.8; // 20% off
showBadge("🔥 Boost Streak: 3 Sessions");
}
Sicherheits-Überlegungen
Webhook Verification
✅ Stripe Signatur muss validiert sein
✅ Webhook muss idempotent sein (mehrfaches Trigger = selbes Resultat)
✅ Race Condition handling: 2 Boosts gleichzeitig = korrekte Position Update
// functions/stripeWebhook.js - Idempotency
export const handler = async (event) => {
const paymentIntentId = stripeEvent.data.object.id;
// Check if we've already processed this
const existing = await supabase
.from('song_boosts')
.select('id')
.eq('payment_intent_id', paymentIntentId)
.single();
if (existing) {
// Already processed, return success (don't duplicate)
return { statusCode: 200, body: 'Webhook already processed' };
}
// Process new boost...
};
Token/Session Security
- Frontend sollte
confirmCardPaymentnicht direkt clientSecret sehen - Best Practice: Backend erstellt Intent, sendet nur clientSecret für diese Transaction
- Frontend hat keine Zugriff auf Stripe API keys
Mögliche Komplikationen & Edge Cases
1. Saved Payment Methods - PCI Compliance
Wenn wir Payment Methods speichern wollen:
- Breakout speichert NOT die Card
- Stripe speichert die Card + gibt Breakout "payment_method_id"
- Breakout speichert payment_method_id
- Für one-click: Breakout sagt Stripe "use payment_method XYZ"
Impleentation: Stripe Elements mit setup_future_usage: 'on_session'
2. Failed Webhooks
Wenn Webhook fehlschlägt (Network Error, etc.):
- Stripe retries automatisch
- Aber Breakout muss auch "fallback" haben
- Optional: Scheduled Job, der nach 1h unbestätigte Boosts sucht und aktualisiert
3. Concurrency Problem
Wenn 100 User gleichzeitig denselben Song boosten:
- Alle sagen "this boost moves song from #50 to #47"
- Nur der erste ist richtig
- Andere Boosts sollten auch erfolgen aber mit aktualisierte Positions
Solution: PostgreSQL Row Locking oder Transactionen
BEGIN TRANSACTION;
LOCK TABLE song_boosts IN EXCLUSIVE MODE;
-- Calculate position
-- Insert boost
-- Update queue position
COMMIT;
4. Chargebacks & Fraud
- User "boosts" dann chargebacks die Zahlung
- Stripe fraud detection sollte das minimieren
- Breakout sollte Song-Position zurücksetzen wenn Chargeback passiert (webhook event)
Metriken zum Tracken
| Metrik | Target | Überprüfung |
|---|---|---|
| Boost Conversion Rate | > 2% nach 2 Wochen | daily |
| Repeat Boost Rate | > 30% of users boost 2+ times | weekly |
| Average Boost Value | €5+ | weekly |
| Boost Revenue / Session | €50+ durchschnittlich | weekly |
| Payment Error Rate | < 0,5% | daily |
| Bounce Rate (Stripe Checkout Abbruch) | < 40% | weekly |
| Saved Payment Method Usage | > 60% Repeat Purchases | weekly |
Implementation Timeline
| Phase | Komponenten | Effort | Timeline |
|---|---|---|---|
| 1. Current State | Stripe Checkout Sessions (existing) | 0 | Now |
| 2. Quick Win | Add Pre-Checkout Screen (position preview + social proof) | 2 days | Week 1 |
| 3. Elements | Stripe Elements in-app payment | 3 days | Week 1-2 |
| 4. Saved Cards | Payment Method Saving + one-click | 2 days | Week 2 |
| 5. Pricing Optimization | Dynamic pricing + A/B testing | 2 days | Week 2 |
| 6. Analytics | Boost metrics tracking | 1 day | Week 3 |
Fazit
Aktueller Flow ist solid aber konvertiert zu niedrig.
Quick Wins (sofort machen):
- Add Position Preview vor Checkout (15 min change)
- Add Social Proof ("247 boosts today") (15 min change)
- Add Urgency ("Queue closes in 12m") (15 min change)
- Tiered Pricing (€2/€5/€10) (2 hours change)
Big Wins (nächste 2 Wochen):
- Stripe Elements für in-app payment (3 days)
- Saved payment methods (2 days)
- Dynamic pricing (1 day)
- A/B testing setup (1 day)
Expected Impact: 2-3x höhere Boost-Conversion, 2x höheres Average-Revenue-per-Boost
Links
MOC - Map of Content | Aktueller Stand der App | Revenue-Streams | Quick Wins | Strategische Entscheidungen