refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,207 @@
# Stripe MCP Server für Claude Code einrichten
## Übersicht
Claude Code unterstützt MCP Server auf drei Ebenen:
1. **Project Scope** (`.mcp.json`) - Für dieses Projekt
2. **User Scope** (`~/.claude.json`) - Global für alle Projekte
3. **Local Scope** - Nur für aktuelle Session
## ✅ Project Setup (bereits erledigt!)
Die `.mcp.json` Datei im Projekt wurde bereits konfiguriert:
```json
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": [
"-y",
"@stripe/mcp",
"--tools=all",
"--api-key=${STRIPE_SECRET_KEY:-sk_test_REPLACE_WITH_YOUR_KEY}"
]
}
}
}
```
### API Key setzen (2 Optionen):
#### Option 1: Environment Variable (empfohlen)
```bash
export STRIPE_SECRET_KEY=sk_test_YOUR_ACTUAL_KEY
```
#### Option 2: Direkt in .mcp.json ersetzen
Ersetze `sk_test_REPLACE_WITH_YOUR_KEY` mit deinem echten Key.
## Global Setup (für alle Projekte)
### 1. Globale Config erstellen
```bash
# Erstelle ~/.claude.json falls nicht vorhanden
touch ~/.claude.json
```
### 2. Stripe MCP hinzufügen
Füge folgendes zu `~/.claude.json` hinzu:
```json
{
"mcpServers": {
"stripe-global": {
"command": "npx",
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_YOUR_GLOBAL_KEY"]
}
}
}
```
### 3. Claude Code neustarten
Nach Änderungen an MCP Konfigurationen solltest du Claude Code neustarten.
## Verfügbare Stripe Tools
Mit dem MCP Server hast du Zugriff auf:
### Customers
- `customers.create` - Kunden erstellen
- `customers.read` - Kunden abrufen
- `customers.update` - Kunden aktualisieren
- `customers.delete` - Kunden löschen
- `customers.list` - Alle Kunden auflisten
### Products & Prices
- `products.create` - Produkte erstellen
- `products.update` - Produkte bearbeiten
- `prices.create` - Preise definieren
- `prices.list` - Preise auflisten
### Subscriptions
- `subscriptions.create` - Abos erstellen
- `subscriptions.update` - Abos ändern
- `subscriptions.cancel` - Abos kündigen
- `subscriptions.list` - Abos auflisten
### Payments
- `paymentLinks.create` - Payment Links generieren
- `checkout.sessions.create` - Checkout Sessions erstellen
### Invoices
- `invoices.read` - Rechnungen abrufen
- `invoices.list` - Rechnungen auflisten
## Test-Befehle für Claude Code
Sage mir einfach:
```markdown
"Verwende den Stripe MCP Server um ein Test-Produkt zu erstellen"
```
Ich sollte antworten können mit:
```
✅ Produkt erstellt: prod_xyz123
```
## Für ulo.ad spezifisch
```markdown
"Verwende den Stripe MCP Server um folgendes für ulo.ad zu erstellen:
1. Produkt 'ulo.ad Pro' mit Beschreibung
2. Monatspreis 9,99€
3. Jahrespreis 99€ (2 Monate gratis)
4. Speichere alle IDs in .env.stripe"
```
## Sicherheitshinweise
### Test vs Production Keys
- **Test Mode**: Keys beginnen mit `sk_test_`
- **Live Mode**: Keys beginnen mit `sk_live_` oder `rk_live_` (restricted)
### Restricted Keys erstellen
Für Production solltest du einen Restricted Key verwenden:
1. Stripe Dashboard → API Keys → Restricted Keys
2. Create Restricted Key
3. Nur diese Permissions aktivieren:
- Customers: Write
- Products: Write
- Prices: Write
- Subscriptions: Write
- Checkout Sessions: Write
### Environment Variables
Nutze Environment Variables statt Keys direkt in Config:
```bash
# .env.local
STRIPE_SECRET_KEY=sk_test_xxx
# Dann in .mcp.json
"--api-key=${STRIPE_SECRET_KEY}"
```
## Troubleshooting
### "MCP server stripe not found"
→ Claude Code neustarten nach Config-Änderung
### "Invalid API key provided"
→ Prüfe ob Key mit `sk_test_` oder `sk_live_` beginnt
### Server startet nicht
Test manuell:
```bash
npx -y @stripe/mcp --tools=all --api-key=sk_test_YOUR_KEY
```
### Permissions Error
→ Verwende Restricted Key mit korrekten Permissions
## Status Check
Um zu prüfen ob alles funktioniert:
1. **In Claude Code**: "List alle verfügbaren MCP Server"
2. **Stripe Test**: "Verwende Stripe MCP um die API zu testen"
## Nächste Schritte
1. ✅ Project MCP Config (`.mcp.json`) - Bereits erledigt!
2. ⏳ Stripe API Key in Environment setzen
3. ⏳ Test mit einem Produkt erstellen
4. ⏳ Alle ulo.ad Produkte automatisch anlegen lassen
## Zusammenfassung
- **Project Setup**: ✅ Fertig in `.mcp.json`
- **Global Setup**: Optional in `~/.claude.json`
- **API Key**: Muss noch gesetzt werden
- **Tools**: Alle Stripe-Funktionen verfügbar
Nach dem Setzen des API Keys kann ich direkt mit der Stripe API arbeiten und alle Produkte, Preise und Konfigurationen für ulo.ad automatisch erstellen!

View file

@ -0,0 +1,786 @@
# Stripe Integration Code Snippets
## Komplette Code-Beispiele für Copy & Paste
### 1. Complete Checkout API Route
```typescript
// src/routes/api/stripe/checkout/+server.ts
import { json } from '@sveltejs/kit';
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY } from '$env/static/private';
import { PUBLIC_APP_URL } from '$env/static/public';
import type { RequestHandler } from './$types';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20'
});
export const POST: RequestHandler = async ({ request, locals }) => {
try {
// Verify authentication
if (!locals.pb.authStore.isValid) {
return json({ error: 'Bitte erst einloggen' }, { status: 401 });
}
const user = locals.pb.authStore.model;
const { interval = 'month' } = await request.json();
// Check if user already has active subscription
if (user?.subscription_status === 'pro') {
return json({ error: 'Du hast bereits ein aktives Abo' }, { status: 400 });
}
// Create or get Stripe customer
let stripeCustomerId = user?.stripe_customer_id;
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name || undefined,
metadata: {
pocketbase_id: user.id,
username: user.username || ''
}
});
stripeCustomerId = customer.id;
// Save customer ID for future use
await locals.pb.collection('users').update(user.id, {
stripe_customer_id: stripeCustomerId
});
}
// Choose price based on interval
const priceId = interval === 'year' ? STRIPE_PRICE_YEARLY : STRIPE_PRICE_MONTHLY;
// Create Stripe Checkout session
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
payment_method_types: ['card', 'sepa_debit', 'paypal'],
billing_address_collection: 'required',
line_items: [
{
price: priceId,
quantity: 1
}
],
mode: 'subscription',
allow_promotion_codes: true,
subscription_data: {
metadata: {
pocketbase_user_id: user.id
}
},
success_url: `${PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${PUBLIC_APP_URL}/pricing?cancelled=true`,
locale: 'de',
metadata: {
user_id: user.id,
user_email: user.email
}
});
return json({
sessionId: session.id,
url: session.url
});
} catch (error) {
console.error('Stripe checkout error:', error);
return json(
{
error: 'Fehler beim Erstellen der Checkout-Session'
},
{ status: 500 }
);
}
};
```
### 2. Complete Webhook Handler
```typescript
// src/routes/api/stripe/webhook/+server.ts
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '$env/static/private';
import type { RequestHandler } from './$types';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20'
});
export const POST: RequestHandler = async ({ request, locals }) => {
const body = await request.text();
const signature = request.headers.get('stripe-signature');
if (!signature) {
return new Response('No signature', { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET);
} catch (err: any) {
console.error(`Webhook signature verification failed: ${err.message}`);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Handle different event types
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
console.log('✅ Checkout completed:', session.id);
const userId = session.metadata?.user_id;
if (!userId) {
console.error('No user_id in session metadata');
break;
}
// Get subscription details
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
// Update user in PocketBase
await locals.pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_customer_id: session.customer,
stripe_subscription_id: subscription.id,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
subscription_interval: subscription.items.data[0].price.recurring?.interval || 'month'
});
// Reset usage counter for new subscribers
await locals.pb.collection('users').update(userId, {
links_created_this_month: 0,
monthly_reset_date: new Date(
new Date().getFullYear(),
new Date().getMonth() + 1,
1
).toISOString()
});
console.log(`User ${userId} upgraded to Pro`);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
console.log('📝 Subscription updated:', subscription.id);
// Get PocketBase user ID from customer
const customer = (await stripe.customers.retrieve(
subscription.customer as string
)) as Stripe.Customer;
const userId = customer.metadata?.pocketbase_id;
if (!userId) {
console.error('No pocketbase_id in customer metadata');
break;
}
// Map Stripe status to our status
let status = 'free';
switch (subscription.status) {
case 'active':
status = 'pro';
break;
case 'past_due':
status = 'past_due';
break;
case 'canceled':
case 'unpaid':
status = 'cancelled';
break;
}
// Update user subscription status
await locals.pb.collection('users').update(userId, {
subscription_status: status,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString()
});
console.log(`User ${userId} subscription status: ${status}`);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
console.log('❌ Subscription cancelled:', subscription.id);
const customer = (await stripe.customers.retrieve(
subscription.customer as string
)) as Stripe.Customer;
const userId = customer.metadata?.pocketbase_id;
if (!userId) {
console.error('No pocketbase_id in customer metadata');
break;
}
// Downgrade user to free
await locals.pb.collection('users').update(userId, {
subscription_status: 'free',
stripe_subscription_id: null,
current_period_end: null
});
console.log(`User ${userId} downgraded to Free`);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
console.log('💳 Payment failed:', invoice.id);
const customer = (await stripe.customers.retrieve(
invoice.customer as string
)) as Stripe.Customer;
const userId = customer.metadata?.pocketbase_id;
if (!userId) break;
// Mark as past_due
await locals.pb.collection('users').update(userId, {
subscription_status: 'past_due'
});
// TODO: Send email notification to user
console.log(`User ${userId} payment failed`);
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
console.log('✅ Payment succeeded:', invoice.id);
// If user was past_due, reactivate
const customer = (await stripe.customers.retrieve(
invoice.customer as string
)) as Stripe.Customer;
const userId = customer.metadata?.pocketbase_id;
if (!userId) break;
const user = await locals.pb.collection('users').getOne(userId);
if (user.subscription_status === 'past_due') {
await locals.pb.collection('users').update(userId, {
subscription_status: 'pro'
});
console.log(`User ${userId} reactivated after payment`);
}
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response('Webhook processed', { status: 200 });
} catch (error) {
console.error('Webhook processing error:', error);
return new Response('Webhook processing failed', { status: 500 });
}
};
```
### 3. Customer Portal Route
```typescript
// src/routes/api/stripe/portal/+server.ts
import { json } from '@sveltejs/kit';
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY } from '$env/static/private';
import { PUBLIC_APP_URL } from '$env/static/public';
import type { RequestHandler } from './$types';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20'
});
export const POST: RequestHandler = async ({ locals }) => {
try {
if (!locals.pb.authStore.isValid) {
return json({ error: 'Nicht authentifiziert' }, { status: 401 });
}
const user = locals.pb.authStore.model;
if (!user?.stripe_customer_id) {
return json({ error: 'Kein Stripe-Kunde gefunden' }, { status: 400 });
}
// Create portal session
const session = await stripe.billingPortal.sessions.create({
customer: user.stripe_customer_id,
return_url: `${PUBLIC_APP_URL}/account`,
locale: 'de'
});
return json({ url: session.url });
} catch (error) {
console.error('Portal session error:', error);
return json({ error: 'Fehler beim Erstellen der Portal-Session' }, { status: 500 });
}
};
```
### 4. Usage Tracking Service
```typescript
// src/lib/server/subscription.ts
import type { PocketBase } from 'pocketbase';
export class SubscriptionService {
constructor(private pb: PocketBase) {}
async canCreateLink(userId: string): Promise<{ allowed: boolean; reason?: string }> {
const user = await this.pb.collection('users').getOne(userId);
// Pro users have unlimited access
if (user.subscription_status === 'pro') {
return { allowed: true };
}
// Past due users should pay first
if (user.subscription_status === 'past_due') {
return {
allowed: false,
reason: 'Bitte aktualisiere deine Zahlungsmethode'
};
}
// Check monthly limit for free users
await this.checkAndResetMonthlyCounter(userId, user);
const updatedUser = await this.pb.collection('users').getOne(userId);
const linksUsed = updatedUser.links_created_this_month || 0;
if (linksUsed >= 10) {
return {
allowed: false,
reason: `Du hast bereits ${linksUsed} von 10 kostenlosen Links diesen Monat erstellt`
};
}
return { allowed: true };
}
async incrementUsage(userId: string): Promise<void> {
const user = await this.pb.collection('users').getOne(userId);
// Don't count for pro users
if (user.subscription_status === 'pro') return;
const currentCount = user.links_created_this_month || 0;
await this.pb.collection('users').update(userId, {
links_created_this_month: currentCount + 1
});
// Log usage
await this.pb.collection('usage_logs').create({
user: userId,
action: 'link_created',
timestamp: new Date().toISOString()
});
}
private async checkAndResetMonthlyCounter(userId: string, user: any): Promise<void> {
const now = new Date();
const resetDate = user.monthly_reset_date ? new Date(user.monthly_reset_date) : null;
// Reset if needed (first of the month or no reset date)
if (!resetDate || resetDate <= now) {
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1);
await this.pb.collection('users').update(userId, {
links_created_this_month: 0,
monthly_reset_date: nextReset.toISOString()
});
}
}
async getUsageStats(userId: string): Promise<{
used: number;
limit: number;
unlimited: boolean;
daysUntilReset: number;
}> {
const user = await this.pb.collection('users').getOne(userId);
if (user.subscription_status === 'pro') {
return {
used: 0,
limit: 0,
unlimited: true,
daysUntilReset: 0
};
}
await this.checkAndResetMonthlyCounter(userId, user);
const updatedUser = await this.pb.collection('users').getOne(userId);
const resetDate = new Date(updatedUser.monthly_reset_date);
const now = new Date();
const daysUntilReset = Math.ceil((resetDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
return {
used: updatedUser.links_created_this_month || 0,
limit: 10,
unlimited: false,
daysUntilReset
};
}
}
```
### 5. React/Svelte Components
```svelte
<!-- src/lib/components/PricingCard.svelte -->
<script lang="ts">
import { loadStripe } from '@stripe/stripe-js';
import { PUBLIC_STRIPE_PUBLISHABLE_KEY } from '$env/static/public';
import { page } from '$app/stores';
export let title: string;
export let price: string;
export let interval: 'month' | 'year' = 'month';
export let features: string[] = [];
export let recommended = false;
let loading = false;
let error = '';
async function handleCheckout() {
loading = true;
error = '';
try {
const response = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interval })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Checkout fehlgeschlagen');
}
const { url, sessionId } = await response.json();
if (url) {
window.location.href = url;
} else if (sessionId) {
const stripe = await loadStripe(PUBLIC_STRIPE_PUBLISHABLE_KEY);
const { error: stripeError } = await stripe!.redirectToCheckout({ sessionId });
if (stripeError) throw stripeError;
}
} catch (err: any) {
error = err.message;
loading = false;
}
}
</script>
<div class="card {recommended ? 'border-primary border-2' : 'border'}">
{#if recommended}
<div class="badge badge-primary absolute -top-3 left-1/2 -translate-x-1/2">Empfohlen</div>
{/if}
<div class="card-body">
<h2 class="card-title justify-center">{title}</h2>
<div class="my-4 text-center">
<span class="text-4xl font-bold">{price}</span>
<span class="text-gray-600">/{interval === 'year' ? 'Jahr' : 'Monat'}</span>
</div>
<ul class="mb-6 space-y-2">
{#each features as feature}
<li class="flex items-center gap-2">
<svg class="text-success h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
{feature}
</li>
{/each}
</ul>
{#if error}
<div class="alert alert-error mb-4">
<span>{error}</span>
</div>
{/if}
<button
class="btn {recommended ? 'btn-primary' : 'btn-outline'} w-full"
on:click={handleCheckout}
disabled={loading}
>
{#if loading}
<span class="loading loading-spinner"></span>
Lädt...
{:else}
Jetzt upgraden
{/if}
</button>
</div>
</div>
```
### 6. Hooks for Protection
```typescript
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { SubscriptionService } from '$lib/server/subscription';
export const handle: Handle = async ({ event, resolve }) => {
// Check subscription for API routes
if (event.url.pathname.startsWith('/api/links') && event.request.method === 'POST') {
if (!event.locals.pb.authStore.isValid) {
return new Response('Unauthorized', { status: 401 });
}
const service = new SubscriptionService(event.locals.pb);
const userId = event.locals.pb.authStore.model?.id;
const { allowed, reason } = await service.canCreateLink(userId);
if (!allowed) {
return new Response(
JSON.stringify({
error: reason,
requiresUpgrade: true
}),
{
status: 403,
headers: { 'Content-Type': 'application/json' }
}
);
}
}
return resolve(event);
};
```
### 7. Account Management Page
```svelte
<!-- src/routes/account/+page.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
export let data;
let loadingPortal = false;
async function openCustomerPortal() {
loadingPortal = true;
try {
const response = await fetch('/api/stripe/portal', {
method: 'POST'
});
const { url } = await response.json();
if (url) {
window.location.href = url;
}
} catch (error) {
console.error('Portal error:', error);
} finally {
loadingPortal = false;
}
}
$: isPro = data.user?.subscription_status === 'pro';
$: nextBillingDate = data.user?.current_period_end
? new Date(data.user.current_period_end).toLocaleDateString('de-DE')
: null;
</script>
<div class="container mx-auto p-4">
<h1 class="mb-6 text-2xl font-bold">Account Einstellungen</h1>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Subscription Status</h2>
<div class="stats shadow">
<div class="stat">
<div class="stat-title">Aktueller Plan</div>
<div class="stat-value text-primary">
{isPro ? 'Pro' : 'Free'}
</div>
{#if nextBillingDate}
<div class="stat-desc">Nächste Zahlung: {nextBillingDate}</div>
{/if}
</div>
{#if !isPro}
<div class="stat">
<div class="stat-title">Links diesen Monat</div>
<div class="stat-value">{data.usage?.used || 0}/10</div>
<div class="stat-desc">
Reset in {data.usage?.daysUntilReset || 0} Tagen
</div>
</div>
{/if}
</div>
<div class="card-actions mt-4 justify-end">
{#if isPro}
<button class="btn btn-primary" on:click={openCustomerPortal} disabled={loadingPortal}>
{#if loadingPortal}
<span class="loading loading-spinner"></span>
{/if}
Abo verwalten
</button>
{:else}
<a href="/pricing" class="btn btn-primary"> Upgrade to Pro </a>
{/if}
</div>
</div>
</div>
</div>
```
### 8. Testing Utilities
```typescript
// src/lib/server/stripe-test.ts
import type { PocketBase } from 'pocketbase';
export async function createTestSubscription(pb: PocketBase, userId: string) {
// Simulate Pro subscription for testing
await pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_customer_id: 'cus_test_' + Date.now(),
stripe_subscription_id: 'sub_test_' + Date.now(),
current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
}
export async function simulateWebhook(eventType: string, data: any) {
const response = await fetch('/api/stripe/webhook', {
method: 'POST',
headers: {
'stripe-signature': 'test_signature',
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: eventType,
data: { object: data }
})
});
return response;
}
```
### 9. Migration Script
```typescript
// scripts/migrate-to-stripe.ts
import PocketBase from 'pocketbase';
const pb = new PocketBase('http://127.0.0.1:8090');
async function addStripeFields() {
// Get users collection
const collection = await pb.collections.getOne('users');
// Add new fields
const updatedSchema = [
...collection.schema,
{
name: 'subscription_status',
type: 'select',
options: {
values: ['free', 'pro', 'cancelled', 'past_due']
},
required: true
},
{
name: 'stripe_customer_id',
type: 'text',
required: false
},
{
name: 'stripe_subscription_id',
type: 'text',
required: false
},
{
name: 'current_period_end',
type: 'date',
required: false
},
{
name: 'links_created_this_month',
type: 'number',
min: 0,
required: true
},
{
name: 'monthly_reset_date',
type: 'date',
required: false
}
];
// Update collection
await pb.collections.update('users', {
schema: updatedSchema
});
console.log('✅ Migration completed');
}
addStripeFields().catch(console.error);
```
## Usage Examples
### Check subscription before action
```typescript
// In your API route
const service = new SubscriptionService(locals.pb);
const { allowed, reason } = await service.canCreateLink(userId);
if (!allowed) {
return json({ error: reason, requiresUpgrade: true }, { status: 403 });
}
```
### Display usage in UI
```svelte
{#if $page.data.usage && !$page.data.usage.unlimited}
<div class="alert alert-info">
Du hast {$page.data.usage.used} von {$page.data.usage.limit} Links erstellt
</div>
{/if}
```
### Handle upgrade flow
```typescript
try {
const response = await createLink(data);
// Success
} catch (error) {
if (error.requiresUpgrade) {
goto('/pricing');
}
}
```

View file

@ -0,0 +1,713 @@
# Stripe Integration Implementation Guide für ulo.ad
## Übersicht
Diese Anleitung beschreibt die Implementierung von Stripe Checkout für ulo.ad mit einem Freemium-Modell (10 kostenlose Links pro Monat, danach Pro-Abo erforderlich).
## Architektur-Entscheidungen
- **Payment Method**: Stripe Checkout (gehostete Zahlungsseite)
- **Subscription Model**: Freemium mit monatlichem Pro-Plan
- **Enforcement**: Server-side Paywall mit Link-Counting
- **Database**: PocketBase für User-Status und Usage-Tracking
## 1. Stripe Account Setup
### 1.1 Account erstellen
1. Registriere dich bei [stripe.com](https://stripe.com)
2. Aktiviere Test-Modus für Entwicklung
3. Notiere folgende Keys aus dem Dashboard:
- Publishable Key: `pk_test_...`
- Secret Key: `sk_test_...`
### 1.2 Webhook Endpoint konfigurieren
1. Gehe zu Dashboard → Developers → Webhooks
2. Füge Endpoint hinzu: `https://ulo.ad/api/stripe/webhook`
3. Wähle Events:
- `checkout.session.completed`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `invoice.payment_failed`
4. Notiere Webhook Secret: `whsec_...`
### 1.3 Produkte und Preise anlegen
```javascript
// Im Stripe Dashboard unter "Products":
Produkt: "ulo.ad Pro"
Beschreibung: "Unlimited link creation and premium features"
Preis 1:
- Name: "Monthly"
- Preis: €9.99/Monat
- Price ID: price_monthly_xxx
Preis 2:
- Name: "Yearly"
- Preis: €99/Jahr (2 Monate gratis)
- Price ID: price_yearly_xxx
```
## 2. PocketBase Schema Updates
### 2.1 Users Collection erweitern
```javascript
// Neue Felder für users collection:
{
"subscription_status": {
"type": "select",
"options": ["free", "pro", "cancelled", "past_due"],
"default": "free",
"required": true
},
"stripe_customer_id": {
"type": "text",
"required": false
},
"stripe_subscription_id": {
"type": "text",
"required": false
},
"current_period_end": {
"type": "date",
"required": false
},
"links_created_this_month": {
"type": "number",
"default": 0,
"min": 0,
"required": true
},
"monthly_reset_date": {
"type": "date",
"required": false
}
}
```
### 2.2 Usage Tracking Collection
```javascript
// Neue collection: usage_logs
{
"user": {
"type": "relation",
"collection": "users",
"required": true
},
"action": {
"type": "select",
"options": ["link_created", "link_deleted", "qr_generated"],
"required": true
},
"timestamp": {
"type": "autodate",
"onCreate": true
},
"link_id": {
"type": "relation",
"collection": "links",
"required": false
}
}
```
## 3. Backend Implementation
### 3.1 Environment Variables
```bash
# .env.local
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_MONTHLY=price_monthly_xxx
STRIPE_PRICE_YEARLY=price_yearly_xxx
PUBLIC_APP_URL=http://localhost:5173 # Production: https://ulo.ad
```
### 3.2 Stripe Client Setup
```typescript
// src/lib/server/stripe.ts
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY } from '$env/static/private';
export const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20',
typescript: true
});
```
### 3.3 Checkout Session API
```typescript
// src/routes/api/stripe/checkout/+server.ts
import { json } from '@sveltejs/kit';
import { stripe } from '$lib/server/stripe';
import { PUBLIC_APP_URL } from '$env/static/public';
import { STRIPE_PRICE_MONTHLY } from '$env/static/private';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
try {
const { priceId, userId } = await request.json();
// Verify user is authenticated
if (!locals.pb.authStore.isValid) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const user = locals.pb.authStore.model;
// Create or retrieve Stripe customer
let customerId = user.stripe_customer_id;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
metadata: {
pocketbase_id: user.id
}
});
customerId = customer.id;
// Save customer ID to PocketBase
await locals.pb.collection('users').update(user.id, {
stripe_customer_id: customerId
});
}
// Create checkout session
const session = await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card', 'sepa_debit'],
line_items: [
{
price: priceId || STRIPE_PRICE_MONTHLY,
quantity: 1
}
],
mode: 'subscription',
success_url: `${PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${PUBLIC_APP_URL}/pricing`,
metadata: {
user_id: user.id
},
allow_promotion_codes: true,
billing_address_collection: 'auto',
locale: 'de'
});
return json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error('Checkout session error:', error);
return json({ error: 'Failed to create checkout session' }, { status: 500 });
}
};
```
### 3.4 Webhook Handler
```typescript
// src/routes/api/stripe/webhook/+server.ts
import { stripe } from '$lib/server/stripe';
import { STRIPE_WEBHOOK_SECRET } from '$env/static/private';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
let event;
try {
event = stripe.webhooks.constructEvent(body, signature!, STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return new Response('Webhook Error', { status: 400 });
}
// Handle the event
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
const userId = session.metadata?.user_id;
if (userId) {
await locals.pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_subscription_id: session.subscription,
current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // Approximate, will be updated by subscription.updated
});
}
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
const customer = await stripe.customers.retrieve(subscription.customer as string);
if (customer && !customer.deleted && customer.metadata?.pocketbase_id) {
const status =
subscription.status === 'active'
? 'pro'
: subscription.status === 'past_due'
? 'past_due'
: 'cancelled';
await locals.pb.collection('users').update(customer.metadata.pocketbase_id, {
subscription_status: status,
current_period_end: new Date(subscription.current_period_end * 1000)
});
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
const customer = await stripe.customers.retrieve(subscription.customer as string);
if (customer && !customer.deleted && customer.metadata?.pocketbase_id) {
await locals.pb.collection('users').update(customer.metadata.pocketbase_id, {
subscription_status: 'cancelled',
stripe_subscription_id: null
});
}
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
const customer = await stripe.customers.retrieve(invoice.customer as string);
if (customer && !customer.deleted && customer.metadata?.pocketbase_id) {
await locals.pb.collection('users').update(customer.metadata.pocketbase_id, {
subscription_status: 'past_due'
});
// TODO: Send email notification
}
break;
}
}
return new Response('Webhook processed', { status: 200 });
} catch (error) {
console.error('Webhook processing error:', error);
return new Response('Webhook processing failed', { status: 500 });
}
};
```
## 4. Frontend Implementation
### 4.1 Upgrade Button Component
```svelte
<!-- src/lib/components/UpgradeButton.svelte -->
<script lang="ts">
import { loadStripe } from '@stripe/stripe-js';
import { PUBLIC_STRIPE_PUBLISHABLE_KEY } from '$env/static/public';
export let priceId: string | undefined = undefined;
export let buttonText = 'Upgrade to Pro';
export let className = '';
let loading = false;
async function handleUpgrade() {
loading = true;
try {
// Create checkout session
const response = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ priceId })
});
const { sessionId, url } = await response.json();
if (url) {
// Redirect to Stripe Checkout
window.location.href = url;
} else {
// Fallback to Stripe.js redirect
const stripe = await loadStripe(PUBLIC_STRIPE_PUBLISHABLE_KEY);
await stripe?.redirectToCheckout({ sessionId });
}
} catch (error) {
console.error('Upgrade error:', error);
// TODO: Show error toast
} finally {
loading = false;
}
}
</script>
<button
on:click={handleUpgrade}
disabled={loading}
class="btn btn-primary {className}"
class:loading
>
{#if loading}
Lädt...
{:else}
{buttonText}
{/if}
</button>
```
### 4.2 Paywall Banner
```svelte
<!-- src/lib/components/PaywallBanner.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import UpgradeButton from './UpgradeButton.svelte';
export let linksUsed = 0;
export let maxFreeLinks = 10;
$: isNearLimit = linksUsed >= maxFreeLinks - 2;
$: hasReachedLimit = linksUsed >= maxFreeLinks;
</script>
{#if isNearLimit}
<div class="alert {hasReachedLimit ? 'alert-error' : 'alert-warning'} mb-4 shadow-lg">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 flex-shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<div>
<h3 class="font-bold">
{#if hasReachedLimit}
Link-Limit erreicht!
{:else}
Du näherst dich deinem monatlichen Limit
{/if}
</h3>
<div class="text-xs">
Du hast {linksUsed} von {maxFreeLinks} kostenlosen Links diesen Monat verwendet.
{#if hasReachedLimit}
Upgrade auf Pro für unbegrenzte Links!
{/if}
</div>
</div>
</div>
<div class="flex-none">
<UpgradeButton buttonText="Jetzt upgraden" className="btn-sm" />
</div>
</div>
{/if}
```
## 5. Server-side Paywall Enforcement
### 5.1 Usage Tracking Middleware
```typescript
// src/lib/server/usage.ts
import type { PocketBase } from 'pocketbase';
export async function checkUsageLimit(pb: PocketBase, userId: string): Promise<boolean> {
const user = await pb.collection('users').getOne(userId);
// Pro users have no limits
if (user.subscription_status === 'pro') {
return true;
}
// Check monthly reset
const now = new Date();
const resetDate = user.monthly_reset_date ? new Date(user.monthly_reset_date) : null;
if (!resetDate || resetDate < now) {
// Reset counter at the beginning of each month
await pb.collection('users').update(userId, {
links_created_this_month: 0,
monthly_reset_date: new Date(now.getFullYear(), now.getMonth() + 1, 1)
});
return true;
}
// Check if under limit
return user.links_created_this_month < 10;
}
export async function incrementUsage(pb: PocketBase, userId: string) {
const user = await pb.collection('users').getOne(userId);
await pb.collection('users').update(userId, {
links_created_this_month: (user.links_created_this_month || 0) + 1
});
// Log usage
await pb.collection('usage_logs').create({
user: userId,
action: 'link_created',
timestamp: new Date()
});
}
```
### 5.2 API Route Protection
```typescript
// src/routes/api/links/+server.ts (Example)
import { json } from '@sveltejs/kit';
import { checkUsageLimit, incrementUsage } from '$lib/server/usage';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.pb.authStore.isValid) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = locals.pb.authStore.model?.id;
// Check usage limit
const canCreate = await checkUsageLimit(locals.pb, userId);
if (!canCreate) {
return json(
{
error: 'Monthly limit reached. Please upgrade to Pro for unlimited links.',
requiresUpgrade: true
},
{ status: 403 }
);
}
try {
// Create the link
const data = await request.json();
const link = await locals.pb.collection('links').create({
...data,
user: userId
});
// Increment usage counter
await incrementUsage(locals.pb, userId);
return json(link);
} catch (error) {
return json({ error: 'Failed to create link' }, { status: 500 });
}
};
```
## 6. Success & Cancel Pages
### 6.1 Success Page
```svelte
<!-- src/routes/checkout/success/+page.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
let verifying = true;
onMount(async () => {
// Verify the session with backend
const sessionId = $page.url.searchParams.get('session_id');
if (sessionId) {
// Optional: Verify session with backend
// await fetch(`/api/stripe/verify-session/${sessionId}`);
}
// Refresh user data
await fetch('/api/user/refresh');
verifying = false;
// Redirect to dashboard after 3 seconds
setTimeout(() => {
goto('/dashboard');
}, 3000);
});
</script>
<div class="flex min-h-screen items-center justify-center">
<div class="text-center">
{#if verifying}
<div class="loading loading-spinner loading-lg"></div>
<p class="mt-4">Verifiziere deine Zahlung...</p>
{:else}
<div class="mb-4 text-6xl">🎉</div>
<h1 class="mb-2 text-3xl font-bold">Willkommen bei ulo.ad Pro!</h1>
<p class="mb-4 text-lg">Dein Upgrade war erfolgreich.</p>
<p class="text-sm text-gray-600">
Du wirst in wenigen Sekunden zum Dashboard weitergeleitet...
</p>
{/if}
</div>
</div>
```
### 6.2 Cancel Page
```svelte
<!-- src/routes/checkout/cancel/+page.svelte -->
<script lang="ts">
import { goto } from '$app/navigation';
import UpgradeButton from '$lib/components/UpgradeButton.svelte';
</script>
<div class="flex min-h-screen items-center justify-center">
<div class="max-w-md text-center">
<div class="mb-4 text-6xl">🤔</div>
<h1 class="mb-2 text-2xl font-bold">Upgrade abgebrochen</h1>
<p class="mb-6">
Kein Problem! Du kannst jederzeit upgraden, wenn du bereit bist. Bis dahin kannst du weiterhin
10 Links pro Monat kostenlos erstellen.
</p>
<div class="flex justify-center gap-4">
<button on:click={() => goto('/dashboard')} class="btn btn-outline">
Zurück zum Dashboard
</button>
<UpgradeButton buttonText="Nochmal versuchen" />
</div>
</div>
</div>
```
## 7. Testing
### 7.1 Stripe Test Cards
```
Erfolgreiche Zahlung: 4242 4242 4242 4242
Zahlung erfordert Auth: 4000 0025 0000 3155
Zahlung abgelehnt: 4000 0000 0000 9995
SEPA Test IBAN: DE89 3704 0044 0532 0130 00
```
### 7.2 Webhook Testing mit Stripe CLI
```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:5173/api/stripe/webhook
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
```
### 7.3 Test Checklist
- [ ] User kann Checkout Session erstellen
- [ ] Redirect zu Stripe funktioniert
- [ ] Success Page wird nach Zahlung angezeigt
- [ ] User Status wird auf "pro" gesetzt
- [ ] Webhooks werden verarbeitet
- [ ] Link-Limit wird für Free User enforced
- [ ] Pro User haben kein Limit
- [ ] Monthly Reset funktioniert
- [ ] Subscription Cancellation wird verarbeitet
- [ ] Failed Payments werden gehandhabt
## 8. Production Deployment
### 8.1 Environment Variables
```bash
# Production .env
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_live_xxx
PUBLIC_APP_URL=https://ulo.ad
```
### 8.2 Webhook URL Update
1. Stripe Dashboard → Webhooks
2. Update Endpoint URL zu `https://ulo.ad/api/stripe/webhook`
3. Neues Webhook Secret notieren
### 8.3 SSL/HTTPS Requirement
Stripe requires HTTPS in production. Stelle sicher, dass deine Domain SSL-Zertifikate hat.
### 8.4 Monitoring
- Stripe Dashboard für Payment Analytics
- PocketBase Logs für Webhook-Fehler
- User Feedback für UX-Probleme
## 9. Rechtliche Anforderungen (Deutschland)
### 9.1 Impressum & AGB Updates
- Zahlungsdienstleister erwähnen
- Widerrufsrecht für digitale Dienstleistungen
- Preise inkl. MwSt. anzeigen
### 9.2 Rechnungsstellung
Stripe kann automatisch Rechnungen erstellen:
1. Dashboard → Settings → Billing → Invoices
2. Aktiviere "Email customers their invoices"
3. Konfiguriere Rechnungsnummer-Format
4. Füge Steuernummer hinzu
### 9.3 DSGVO
- Erwähne Stripe in Datenschutzerklärung
- Datenverarbeitung in den USA erwähnen
- Auftragsverarbeitungsvertrag mit Stripe
## Support & Troubleshooting
### Häufige Probleme
**Webhook 400 Error**
- Webhook Secret prüfen
- Body als raw text, nicht JSON parsen
**User Status wird nicht aktualisiert**
- Webhook Events in Stripe Dashboard prüfen
- PocketBase Permissions prüfen
**CORS Fehler**
- PUBLIC_APP_URL korrekt setzen
- Stripe Checkout erlaubt keine custom headers
### Hilfreiche Links
- [Stripe Docs](https://stripe.com/docs)
- [Stripe Checkout Guide](https://stripe.com/docs/payments/checkout)
- [Webhook Best Practices](https://stripe.com/docs/webhooks/best-practices)
- [SvelteKit + Stripe Example](https://github.com/stripe-samples/subscription-use-cases/tree/main/fixed-price-subscriptions/sveltekit)

View file

@ -0,0 +1,136 @@
# Stripe MCP Server in 5 Minuten
## Was das ist
Mit dem MCP Server kann Claude direkt Stripe-Produkte, Preise und alles andere für dich anlegen. Du musst nichts manuell im Dashboard machen.
## Setup (3 Minuten)
### 1. Stripe API Key holen
- Gehe zu [stripe.com/dashboard](https://dashboard.stripe.com)
- → Developers → API Keys
- Kopiere den **Secret Key** (sk*test*...)
### 2. Claude Desktop Config öffnen
**macOS:**
```bash
open ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
**Windows:**
```bash
notepad %APPDATA%\Claude\claude_desktop_config.json
```
### 3. Config einfügen
Ersetze die komplette Datei mit:
```json
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_DEIN_KEY_HIER"]
}
}
}
```
⚠️ **Ersetze `sk_test_DEIN_KEY_HIER` mit deinem echten Stripe Key!**
### 4. Claude neustarten
- Claude Desktop komplett beenden (CMD+Q / Alt+F4)
- Claude Desktop wieder öffnen
## Fertig! Das war's! ✅
## Test (1 Minute)
Sage zu Claude:
```
"Verwende den Stripe MCP Server um ein Produkt namens 'Test Product' für 9.99€ zu erstellen"
```
Claude sollte antworten mit etwas wie:
```
✅ Produkt erstellt: prod_xyz123
✅ Preis erstellt: price_abc456 (9.99€/Monat)
```
## Was Claude jetzt kann
```markdown
"Erstelle ein Produkt ulo.ad Pro für 9.99€/Monat"
"Liste alle meine Stripe Kunden"
"Erstelle einen Test-Kunden"
"Zeige mir alle aktiven Subscriptions"
"Erstelle einen Payment Link für das Pro Produkt"
```
## Für ulo.ad Setup
Sage einfach zu Claude:
```markdown
"Verwende den Stripe MCP Server um folgendes zu erstellen:
1. Produkt 'ulo.ad Pro'
2. Monatspreis 9,99€
3. Jahrespreis 99€
4. Gib mir alle IDs für meine .env Datei"
```
Claude erledigt alles automatisch und gibt dir:
```
STRIPE_PRODUCT_ID=prod_xyz
STRIPE_PRICE_MONTHLY=price_abc
STRIPE_PRICE_YEARLY=price_def
```
## Troubleshooting (falls nötig)
### "MCP Server nicht gefunden"
→ Claude komplett neustarten (nicht nur Fenster schließen)
### "Invalid API Key"
→ Key muss mit `sk_test_` beginnen
### "Permission denied"
→ Prüfe ob der Key korrekt in der Config steht
### Config Datei existiert nicht
→ Erstelle sie einfach neu:
```bash
# macOS
mkdir -p ~/Library/Application\ Support/Claude
echo '{"mcpServers":{}}' > ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
## Sicherheitstipp
Für Production verwende einen **Restricted Key**:
1. Stripe Dashboard → API Keys → Restricted Keys → Create
2. Nur diese Permissions:
- Products: Write
- Prices: Write
- Customers: Read
3. Verwende diesen Key statt dem Secret Key
## Das war wirklich alles! 🚀
Keine weitere Konfiguration nötig. Claude kann jetzt direkt mit Stripe arbeiten.

View file

@ -0,0 +1,413 @@
# Stripe MCP Server Integration Guide
## Was ist der Stripe MCP Server?
Der Stripe Model Context Protocol (MCP) Server ist eine offizielle Implementierung von Stripe, die es AI-Assistenten wie Claude ermöglicht, direkt mit der Stripe API zu interagieren. Dies bedeutet, dass Claude automatisch Stripe-Produkte, Preise, Kunden und Subscriptions für dich anlegen und verwalten kann.
## Vorteile des MCP Servers
### Automatisierung
- Claude kann selbstständig Stripe-Ressourcen erstellen (Produkte, Preise, Kunden)
- Automatisches Setup von Subscriptions und Payment Links
- Direkte API-Interaktion ohne manuelles Copy & Paste
### Zeitersparnis
- Keine manuelle Arbeit im Stripe Dashboard nötig
- Claude kann alle Stripe-Objekte programmatisch erstellen
- Sofortige Validierung und Testing möglich
### Fehlerreduktion
- Konsistente Namensgebung und Struktur
- Automatische Verknüpfung von IDs
- Direkte Fehlerbehandlung durch Claude
## Installation & Setup
### Option 1: Remote MCP Server (Empfohlen)
Stripe hostet einen offiziellen MCP Server unter `https://mcp.stripe.com`. Dies ist der einfachste Weg:
```json
// claude_desktop_config.json
{
"mcpServers": {
"stripe": {
"type": "remote",
"url": "https://mcp.stripe.com",
"auth": {
"type": "bearer",
"token": "sk_test_YOUR_STRIPE_SECRET_KEY"
}
}
}
}
```
### Option 2: Lokaler MCP Server via NPX
Du kannst den Stripe MCP Server auch lokal ausführen:
```bash
# Alle Tools aktivieren
npx -y @stripe/mcp --tools=all --api-key=sk_test_YOUR_KEY
# Nur spezifische Tools
npx -y @stripe/mcp --tools=customers.create,products.create,prices.create --api-key=sk_test_YOUR_KEY
```
Für Claude Desktop Konfiguration:
```json
// claude_desktop_config.json (Lokale Variante)
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_YOUR_STRIPE_SECRET_KEY"]
}
}
}
```
### Option 3: Custom Implementation
Für erweiterte Anpassungen kannst du einen eigenen MCP Server implementieren:
```typescript
// stripe-mcp-server.ts
import { StripeAgentToolkit } from '@stripe/agent-toolkit/modelcontextprotocol';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new StripeAgentToolkit({
secretKey: process.env.STRIPE_SECRET_KEY!,
configuration: {
actions: {
customers: { create: true, read: true, update: true },
products: { create: true, read: true },
prices: { create: true, read: true },
paymentLinks: { create: true },
subscriptions: { create: true, read: true, update: true, cancel: true },
invoices: { read: true },
checkout: { sessions: { create: true } }
}
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
```
## Konfiguration für Claude Desktop
### Schritt 1: Config-Datei finden
Die Konfigurationsdatei befindet sich unter:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
### Schritt 2: Stripe Server hinzufügen
```json
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_51..."]
}
}
}
```
### Schritt 3: Claude Desktop neustarten
Nach dem Speichern der Konfiguration:
1. Claude Desktop komplett beenden (nicht nur schließen)
2. Claude Desktop neu starten
3. In den Settings sollte der Stripe Server sichtbar sein
## Verfügbare Tools & Aktionen
### Customer Management
- `customers.create` - Neue Kunden anlegen
- `customers.read` - Kunden-Informationen abrufen
- `customers.update` - Kunden-Daten aktualisieren
- `customers.list` - Alle Kunden auflisten
### Product & Pricing
- `products.create` - Produkte erstellen
- `products.update` - Produkte bearbeiten
- `prices.create` - Preise definieren
- `prices.list` - Preise auflisten
### Subscriptions
- `subscriptions.create` - Neue Abos erstellen
- `subscriptions.update` - Abos ändern
- `subscriptions.cancel` - Abos kündigen
- `subscriptions.list` - Alle Abos anzeigen
### Payments
- `paymentLinks.create` - Payment Links generieren
- `checkout.sessions.create` - Checkout Sessions erstellen
- `invoices.read` - Rechnungen abrufen
- `invoices.list` - Alle Rechnungen anzeigen
### Webhooks
- `webhooks.create` - Webhook Endpoints erstellen
- `webhooks.list` - Webhooks auflisten
## Praktische Anwendung für ulo.ad
### Automatisches Setup mit Claude
Mit dem MCP Server kann Claude folgende Aufgaben automatisch erledigen:
1. **Produkte anlegen**
```typescript
// Claude kann direkt ausführen:
await stripe.products.create({
name: 'ulo.ad Pro',
description: 'Unlimited link creation and premium features'
});
```
2. **Preise erstellen**
```typescript
// Monatlicher Preis
await stripe.prices.create({
product: 'prod_xxx',
unit_amount: 999,
currency: 'eur',
recurring: { interval: 'month' }
});
```
3. **Webhook Endpoints konfigurieren**
```typescript
await stripe.webhooks.create({
url: 'https://ulo.ad/api/stripe/webhook',
enabled_events: ['checkout.session.completed', 'customer.subscription.updated']
});
```
### Workflow-Beispiel
```markdown
User: "Bitte erstelle die komplette Stripe-Konfiguration für ulo.ad"
Claude (mit MCP Server):
1. ✅ Erstelle Produkt "ulo.ad Pro"
2. ✅ Erstelle Monats-Preis (9,99€)
3. ✅ Erstelle Jahres-Preis (99€)
4. ✅ Konfiguriere Webhook Endpoint
5. ✅ Erstelle Test-Kunde
6. ✅ Generiere erste Checkout Session
7. ✅ Speichere alle IDs in .env
```
## Sicherheit & Best Practices
### API Keys
**WICHTIG**: Verwende immer Restricted API Keys mit minimalen Berechtigungen:
```bash
# Erstelle einen Restricted Key im Stripe Dashboard mit nur den nötigen Permissions:
- Customers: Write
- Products: Write
- Prices: Write
- Subscriptions: Write
- Checkout Sessions: Write
- Webhooks: Read
```
### Test vs. Production
```json
// Entwicklung (Test Mode)
{
"mcpServers": {
"stripe-test": {
"command": "npx",
"args": ["@stripe/mcp", "--api-key=sk_test_..."]
}
}
}
// Production (Live Mode) - Vorsicht!
{
"mcpServers": {
"stripe-live": {
"command": "npx",
"args": ["@stripe/mcp", "--api-key=rk_live_..."] // Restricted Key!
}
}
}
```
### Umgebungsvariablen
Statt API Keys direkt in der Config zu speichern:
```json
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["@stripe/mcp", "--tools=all"],
"env": {
"STRIPE_API_KEY": "${STRIPE_SECRET_KEY}"
}
}
}
}
```
## Troubleshooting
### Server erscheint nicht in Claude
1. **Config-Syntax prüfen**: JSON muss valide sein
2. **Claude komplett neustarten**: Nicht nur Fenster schließen
3. **Logs prüfen**: `~/Library/Logs/Claude/` (macOS)
### Verbindungsfehler
```bash
# Test ob NPX funktioniert
npx -y @stripe/mcp --version
# Manuelle Installation falls nötig
npm install -g @stripe/mcp
```
### API Key Fehler
- Stelle sicher, dass der Key mit `sk_test_` (Test) oder `rk_live_` (Restricted Live) beginnt
- Prüfe Permissions im Stripe Dashboard
- Verwende niemals den publishable key (`pk_`)
## Integration mit ulo.ad Workflow
### Schritt 1: MCP Server aktivieren
```bash
# In Claude Desktop Config hinzufügen
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": [
"-y",
"@stripe/mcp",
"--tools=products.create,prices.create,customers.create,subscriptions.create,checkout.sessions.create",
"--api-key=sk_test_YOUR_KEY"
]
}
}
}
```
### Schritt 2: Claude anweisen
```markdown
"Verwende den Stripe MCP Server um folgendes zu erstellen:
1. Produkt 'ulo.ad Pro' mit Beschreibung
2. Monatspreis 9,99€ und Jahrespreis 99€
3. Speichere alle IDs in einer .env.stripe Datei"
```
### Schritt 3: Automatische Ausführung
Claude wird dann automatisch:
- Die Stripe API aufrufen
- Alle Ressourcen erstellen
- IDs und Konfiguration zurückgeben
- Diese in deinem Projekt speichern
## Vorteile gegenüber manuellem Setup
| Manuell | Mit MCP Server |
| ---------------------------- | -------------------------- |
| 30+ Minuten Dashboard-Arbeit | 2 Minuten automatisch |
| Copy & Paste von IDs | Automatische ID-Verwaltung |
| Fehleranfällig | Konsistent & validiert |
| Mehrere Browser-Tabs | Alles in Claude |
| Manuelles Testing | Sofortige Validierung |
## Erweiterte Features
### Batch-Operationen
```typescript
// Claude kann mehrere Operationen gleichzeitig ausführen
const operations = [
stripe.products.create({ name: 'Basic' }),
stripe.products.create({ name: 'Pro' }),
stripe.products.create({ name: 'Enterprise' })
];
await Promise.all(operations);
```
### Conditional Logic
```typescript
// Claude kann intelligente Entscheidungen treffen
const existingProduct = await stripe.products.list({ limit: 1 });
if (existingProduct.data.length === 0) {
await stripe.products.create({ name: 'ulo.ad Pro' });
}
```
### Testing & Validation
```typescript
// Claude kann automatisch testen
const testSession = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: 'http://localhost:5173/success'
});
console.log('Test Checkout URL:', testSession.url);
```
## Nächste Schritte
1. **MCP Server einrichten**: Config-Datei bearbeiten und Claude neustarten
2. **Test-Projekt**: Lass Claude ein komplettes Stripe-Setup erstellen
3. **Integration**: Verbinde die erstellten Stripe-Ressourcen mit deinem Code
4. **Automation**: Nutze Claude für wiederkehrende Stripe-Aufgaben
## Wichtige Links
- [Stripe Agent Toolkit GitHub](https://github.com/stripe/agent-toolkit)
- [MCP Protocol Docs](https://modelcontextprotocol.io)
- [Stripe API Dokumentation](https://stripe.com/docs/api)
- [Claude Desktop MCP Guide](https://docs.anthropic.com/claude/docs/mcp)
## Zusammenfassung
Der Stripe MCP Server transformiert die Art, wie du mit Stripe arbeitest:
- **Keine manuelle Dashboard-Arbeit mehr**: Alles über Claude
- **Fehlerfreie Konfiguration**: Automatische Validierung
- **Schnelleres Development**: Von Stunden auf Minuten
- **Bessere Developer Experience**: Alles in einem Tool
Mit dem MCP Server wird Claude zu deinem persönlichen Stripe-Administrator, der alle API-Operationen für dich ausführen kann - sicher, schnell und zuverlässig.

View file

@ -0,0 +1,143 @@
Hier ist eine Übersicht
eures Stripe-Setups und
wie es mit der Datenbank
zusammenarbeitet:
Stripe-Setup Architektur
1. Stripe-Konfiguration (
src/lib/server/stripe.ts)
- Initialisiert Stripe
mit API-Schlüssel aus
Umgebungsvariablen
- Definiert drei
Preismodelle: Monthly,
Yearly, Lifetime
- Unterstützt mehrere
Währungen (USD/EUR)
basierend auf Locale
2. Pricing-Struktur
- Monthly: $4.99/Monat
(4,99€)
- Yearly: $39.99/Jahr
(39,99€) - spart $20
- Lifetime: $129.99
einmalig (129,99€)
3. Checkout-Flow
(src/routes/api/stripe/ch
eckout/+server.ts)
Prozess:
1. User muss eingeloggt
sein
2. System prüft
bestehende Subscription
3. Erstellt/findet Stripe
Customer mit Metadaten
(pocketbase_id, username)
4. Erstellt Checkout
Session mit:
- Lokalisation
(DE/EN/IT/FR/ES) - Zahlungsmethoden
(Karte + SEPA) - Promotion Codes
erlaubt - Metadaten für
User-Zuordnung
4. Datenbank-Integration
(PocketBase users
Collection)
Stripe-relevante Felder:
- subscription_status:
free|pro|team|team_plus|c
ancelled|past_due
- stripe_customer_id:
Verknüpfung zum Stripe
Customer
- stripe_subscription_id:
Verknüpfung zur Stripe
Subscription
- current_period_end:
Ablaufdatum der aktuellen
Periode
- links_created_this_month:
Limitierung für Free
Users
5. Webhook-Handler
(src/routes/api/stripe/we
bhook/+server.ts)
Behandelte Events:
- checkout.session.comple
ted: Aktiviert Pro-Status
nach erfolgreicher
Zahlung
- customer.subscription.u
pdated: Aktualisiert
Subscription-Status
- customer.subscription.d
eleted: Downgrade zu Free
- invoice.payment_failed:
Setzt Status auf
past_due
- invoice.payment_succeed
ed: Reaktiviert nach
erfolgreicher Zahlung
Besonderheiten:
- Lifetime-Käufe:
Erhalten subscription*id:
"lifetime*" + session.id
und Ablaufdatum 2099
- Admin-Authentifizierung:
Webhooks verwenden
Admin-Credentials für
Datenbank-Updates
6. Mehrsprachigkeit
- Vollständige
Übersetzungen
(DE/EN/IT/FR/ES) in
stripe-translations.ts
- Automatische
Währungsauswahl (EUR für
EU, USD für Rest)
- Stripe Checkout in
Benutzersprache
7. Sicherheit &
Fehlerbehandlung
- Webhook-Signatur-Verifi
zierung
- Fallback für
Entwicklungsumgebung ohne
Webhook-Secret
- Umfassende
Fehlerbehandlung mit
benutzerfreundlichen
Meldungen
- Admin-Authentifizierung
für kritische
Operationen
Das System ermöglicht
nahtlose Übergänge
zwischen Free- und
Pro-Status basierend auf
Stripe-Events und hält
die PocketBase-Datenbank
automatisch synchron.

View file

@ -0,0 +1,276 @@
# Stripe Quick Start Guide
## 🚀 Schnellstart in 30 Minuten
Diese Anleitung bringt dich in 30 Minuten zu einer funktionierenden Stripe-Integration.
## Prerequisites
- Stripe Account (stripe.com)
- SvelteKit Projekt läuft (`npm run dev`)
- PocketBase läuft
## Step 1: Stripe Setup (5 Min)
### 1.1 Install Stripe Dependencies
```bash
npm install stripe @stripe/stripe-js
```
### 1.2 Get API Keys
1. Login bei [stripe.com/dashboard](https://dashboard.stripe.com)
2. Kopiere Test Keys:
- Publishable Key: `pk_test_...`
- Secret Key: `sk_test_...`
### 1.3 Create Product
Im Stripe Dashboard:
1. Products → Add Product
2. Name: "ulo.ad Pro"
3. Price: €9.99/month
4. Kopiere Price ID: `price_xxx`
## Step 2: Environment Setup (2 Min)
```bash
# .env.local
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51...
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_PRICE_ID=price_1...
PUBLIC_APP_URL=http://localhost:5173
```
## Step 3: Minimal Backend (10 Min)
### 3.1 Stripe Client
```typescript
// src/lib/server/stripe.ts
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY } from '$env/static/private';
export const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2024-11-20'
});
```
### 3.2 Checkout Endpoint
```typescript
// src/routes/api/checkout/+server.ts
import { json } from '@sveltejs/kit';
import { stripe } from '$lib/server/stripe';
import { PUBLIC_APP_URL } from '$env/static/public';
import { STRIPE_PRICE_ID } from '$env/static/private';
export async function POST({ locals }) {
// Check if user is logged in
if (!locals.pb.authStore.isValid) {
return json({ error: 'Please login first' }, { status: 401 });
}
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: STRIPE_PRICE_ID,
quantity: 1
}
],
mode: 'subscription',
success_url: `${PUBLIC_APP_URL}/success`,
cancel_url: `${PUBLIC_APP_URL}/`,
client_reference_id: locals.pb.authStore.model?.id
});
return json({ url: session.url });
}
```
## Step 4: Minimal Frontend (5 Min)
### 4.1 Upgrade Button
```svelte
<!-- src/lib/components/QuickUpgrade.svelte -->
<script lang="ts">
let loading = false;
async function upgrade() {
loading = true;
const res = await fetch('/api/checkout', { method: 'POST' });
const { url } = await res.json();
window.location.href = url;
}
</script>
<button on:click={upgrade} disabled={loading}>
{loading ? 'Loading...' : 'Upgrade to Pro €9.99/mo'}
</button>
```
### 4.2 Success Page
```svelte
<!-- src/routes/success/+page.svelte -->
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
onMount(() => {
setTimeout(() => goto('/'), 3000);
});
</script>
<div>
<h1>🎉 Payment Successful!</h1>
<p>Redirecting to dashboard...</p>
</div>
```
## Step 5: Test Payment (3 Min)
1. Click "Upgrade to Pro"
2. Use test card: `4242 4242 4242 4242`
3. Any future date, any CVC
4. Complete payment
5. Check Stripe Dashboard for payment
## Step 6: Basic Webhook (5 Min)
### 6.1 Get Webhook Secret
```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login and forward webhooks
stripe login
stripe listen --forward-to localhost:5173/api/webhook
# Copy the webhook secret displayed
```
### 6.2 Webhook Handler
```typescript
// src/routes/api/webhook/+server.ts
import { stripe } from '$lib/server/stripe';
import { STRIPE_WEBHOOK_SECRET } from '$env/static/private';
export async function POST({ request, locals }) {
const body = await request.text();
const sig = request.headers.get('stripe-signature')!;
try {
const event = stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const userId = session.client_reference_id;
// Update user to Pro in PocketBase
if (userId) {
await locals.pb.collection('users').update(userId, {
subscription_status: 'pro'
});
}
}
return new Response('OK');
} catch (err) {
return new Response('Webhook Error', { status: 400 });
}
}
```
## Step 7: Add to PocketBase (2 Min)
Add field to users collection:
```javascript
{
name: "subscription_status",
type: "select",
options: ["free", "pro"],
default: "free"
}
```
## Done! 🎊
Du hast jetzt:
- ✅ Funktionierende Checkout-Seite
- ✅ Test-Zahlungen möglich
- ✅ User-Status wird aktualisiert
- ✅ Webhook-Integration
## Next Steps
### Enforce Limits
```typescript
// In your API routes
if (user.subscription_status !== 'pro' && user.links_count >= 10) {
return json({ error: 'Upgrade to Pro for unlimited links' }, { status: 403 });
}
```
### Add Customer Portal
```typescript
const session = await stripe.billingPortal.sessions.create({
customer: user.stripe_customer_id,
return_url: `${PUBLIC_APP_URL}/account`
});
```
### Production Checklist
- [ ] Switch to live keys
- [ ] Update webhook endpoint
- [ ] Add error handling
- [ ] Add loading states
- [ ] Test mit echten Karten
- [ ] Setup email notifications
## Troubleshooting
**"No such price"**
→ Check STRIPE_PRICE_ID in .env
**Webhook 400 Error**
→ Wrong webhook secret, check stripe listen output
**User not updated**
→ Check PocketBase permissions
**Checkout won't load**
→ Check PUBLIC_STRIPE_PUBLISHABLE_KEY
## Useful Commands
```bash
# Watch webhook events
stripe listen --forward-to localhost:5173/api/webhook
# Trigger test events
stripe trigger checkout.session.completed
# See recent events
stripe events list
# Create test customer
stripe customers create --email=test@example.com
```
## Links
- [Stripe Test Cards](https://stripe.com/docs/testing#cards)
- [Checkout Docs](https://stripe.com/docs/payments/checkout)
- [SvelteKit Example](https://github.com/stripe-samples/checkout-single-subscription)

View file

@ -0,0 +1,327 @@
# Der einfachste Weg: Stripe in 1 Stunde integrieren
## Das Ziel
10 kostenlose Links pro Monat, danach 9,99€/Monat für unbegrenzte Links.
## Was du brauchst (5 Min)
### 1. Stripe Account
- Gehe zu [stripe.com](https://stripe.com) → Registrieren
- Wähle "Test Mode" (oben rechts)
- Kopiere deine Test Keys aus dem Dashboard
### 2. NPM Package
```bash
npm install stripe @stripe/stripe-js
```
### 3. Environment Variables
```bash
# .env
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RujJlPRtmsJbOMgKRJu4uOqOGzGXwI8FT0qwf1jJUQ0HJIDmxBR3fzJSqGhVQCJ5xAJ4jKh0U6JvfLdx76FpMGB00xQI2j4qg
STRIPE_SECRET_KEY=sk_test_51RujJlPRtmsJbOMgPQDqEA4CBgWSGKkjlCry8nTlHs9b6xSwwh0ccj6RoaZSvl84cQ8TO28Nk0ug64fRlF978vK300EsYm8RP0
# Stripe Product & Prices
STRIPE_PRODUCT_ID=prod_SrqNlCbfaaKSnk
STRIPE_PRICE_MONTHLY=price_1Rw6hkPRtmsJbOMgdUYfj7ee
STRIPE_PRICE_YEARLY=price_1Rw6j0PRtmsJbOMgTGrOZH2c
STRIPE_PRICE_LIFETIME=price_1Rw6qPPRtmsJbOMgsS6nnBTM
PUBLIC_APP_URL=http://localhost:5173
```
## Schritt 1: Produkt in Stripe erstellen (2 Min)
Im Stripe Dashboard:
1. → Products → Add Product
2. Name: "ulo.ad Pro"
3. → Add Price: 9,99€ / Monthly
4. **Kopiere die Price ID** (z.B. `price_1OxAbc...`)
5. Füge sie in `.env.local` ein
## Schritt 2: Minimal Backend (10 Min)
### Checkout Route
```typescript
// src/routes/api/stripe/checkout/+server.ts
import { json } from '@sveltejs/kit';
import Stripe from 'stripe';
import { env } from '$env/dynamic/private';
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2024-10-28.acacia'
});
export async function POST({ request, locals, url }) {
// User muss eingeloggt sein
if (!locals.user) {
return json({ error: 'Login required' }, { status: 401 });
}
const { priceType = 'monthly' } = await request.json();
// Preis auswählen
let priceId;
let mode = 'subscription';
switch (priceType) {
case 'yearly':
priceId = env.STRIPE_PRICE_YEARLY;
break;
case 'lifetime':
priceId = env.STRIPE_PRICE_LIFETIME;
mode = 'payment';
break;
default:
priceId = env.STRIPE_PRICE_MONTHLY;
}
const session = await stripe.checkout.sessions.create({
customer_email: locals.user.email,
payment_method_types: ['card', 'sepa_debit'],
line_items: [
{
price: priceId,
quantity: 1
}
],
mode,
success_url: `${url.origin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${url.origin}/pricing`,
client_reference_id: locals.user.id
});
return json({ url: session.url });
}
```
### Webhook Handler (vereinfacht)
```typescript
// src/routes/api/stripe/webhook/+server.ts
import Stripe from 'stripe';
import { env } from '$env/dynamic/private';
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2024-10-28.acacia'
});
export async function POST({ request, locals }) {
const body = await request.text();
// Vereinfacht: Kein Signature Check für Test
const event = JSON.parse(body);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const userId = session.client_reference_id;
// User auf Pro upgraden
await locals.pb.collection('users').update(userId, {
subscription_status: 'pro',
stripe_customer_id: session.customer
});
}
return new Response('OK');
}
```
## Schritt 3: PocketBase Update (5 Min)
In PocketBase Admin:
1. Users Collection → Edit
2. Add Field:
- Name: `subscription_status`
- Type: Select
- Options: `free`, `pro`
- Default: `free`
3. Add Field:
- Name: `links_count`
- Type: Number
- Default: 0
## Schritt 4: Frontend (10 Min)
### Upgrade Button
```svelte
<!-- src/lib/components/UpgradeButton.svelte -->
<script>
export let priceType = 'monthly';
async function upgrade() {
const res = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceType })
});
const { url } = await res.json();
window.location.href = url;
}
</script>
<button on:click={upgrade} class="btn btn-primary">
Upgrade für {priceType === 'yearly' ? '39,99€/Jahr' : '4,99€/Monat'}
</button>
```
### Paywall Check
```svelte
<!-- src/routes/create/+page.svelte -->
<script>
export let data;
$: canCreate = data.user.subscription_status === 'pro' || data.user.links_count < 10;
</script>
{#if !canCreate}
<div class="alert alert-warning">
Du hast dein Limit von 10 kostenlosen Links erreicht.
<UpgradeButton />
</div>
{:else}
<!-- Link erstellen Form -->
{/if}
```
### Success Page
```svelte
<!-- src/routes/success/+page.svelte -->
<script>
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
setTimeout(() => goto('/'), 3000);
});
</script>
<h1>🎉 Willkommen bei Pro!</h1><p>Du wirst gleich weitergeleitet...</p>
```
## Schritt 5: API Protection (5 Min)
```typescript
// src/routes/api/links/+server.ts
export async function POST({ request, locals }) {
const user = locals.user;
// Check limit
if (user.subscription_status !== 'pro' && user.links_count >= 10) {
return json({
error: 'Limit erreicht. Bitte upgrade auf Pro.'
}, { status: 403 });
}
// Link erstellen
const link = await locals.pb.collection('links').create({...});
// Counter erhöhen (nur für Free User)
if (user.subscription_status !== 'pro') {
await locals.pb.collection('users').update(user.id, {
links_count: user.links_count + 1
});
}
return json(link);
}
```
## Schritt 6: Testing (5 Min)
### Webhook mit Stripe CLI testen
```bash
# Stripe CLI installieren
brew install stripe/stripe-cli/stripe
# Webhooks forwarden
stripe listen --forward-to localhost:5173/api/webhook
# In neuem Terminal: Test Event senden
stripe trigger checkout.session.completed
```
### Test Kreditkarte
- Nummer: `4242 4242 4242 4242`
- Datum: Beliebig in Zukunft
- CVC: Beliebige 3 Zahlen
## Fertig! ✅
Das war's. Du hast jetzt:
- ✅ Stripe Checkout für Payments
- ✅ 10 kostenlose Links pro Monat
- ✅ Pro-Abo für unbegrenzte Links
- ✅ Automatisches Status-Update
## Nächste Schritte (Optional)
### Webhook Security hinzufügen
```typescript
// Webhook Signature verifizieren
const sig = request.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET);
```
### Monatliches Reset
```typescript
// Cron Job oder in API Route
if (new Date().getDate() === 1) {
await pb.collection('users').update(userId, { links_count: 0 });
}
```
### Customer Portal
```typescript
// Abo verwalten
const portal = await stripe.billingPortal.sessions.create({
customer: user.stripe_customer_id,
return_url: `${PUBLIC_APP_URL}/account`
});
```
## Probleme?
### "Invalid Stripe API version"
→ Verwende `apiVersion: '2024-10-28.acacia'` beim Stripe initialisieren
### "Price ID not found"
→ Alle Umgebungsvariablen setzen:
- STRIPE_PRICE_MONTHLY
- STRIPE_PRICE_YEARLY
- STRIPE_PRICE_LIFETIME
### "No such price"
→ Price ID aus Stripe Dashboard kopieren
### Webhook funktioniert nicht
`stripe listen` läuft? Terminal Output prüfen
### User wird nicht auf Pro gesetzt
→ PocketBase Permissions prüfen
## Das war's! 🚀
Von 0 auf bezahlte Subscriptions in einer Stunde. Keine Magie, nur die absolut nötigsten Teile.

View file

@ -0,0 +1,29 @@
# Stripe Deployment Setup
## Required Environment Variables
Add these to your deployment platform (e.g., Coolify):
```bash
# Stripe API Keys (Test)
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RujJlPRtmsJbOMgKRJu4uOqOGzGXwI8FT0qwf1jJUQ0HJIDmxBR3fzJSqGhVQCJ5xAJ4jKh0U6JvfLdx76FpMGB00xQI2j4qg
# Stripe Product Configuration
STRIPE_PRODUCT_ID=prod_SrqNlCbfaaKSnk
STRIPE_PRICE_MONTHLY=price_1Rw6hkPRtmsJbOMgdUYfj7ee
STRIPE_PRICE_YEARLY=price_1Rw6j0PRtmsJbOMgTGrOZH2c
STRIPE_PRICE_LIFETIME=price_1Rw6qPPRtmsJbOMgsS6nnBTM
```
## Finding Your Values
1. **API Keys**: Stripe Dashboard → Developers → API Keys
2. **Product ID**: Stripe Dashboard → Products → Select product → Copy ID
3. **Price IDs**: Stripe Dashboard → Products → Select product → Pricing section → Copy price IDs
## Important Notes
- Use test keys (`sk_test_`, `pk_test_`) for development
- Use live keys (`sk_live_`, `pk_live_`) for production
- All variables must be set or checkout will fail with "Price ID not found"
- Restart application after adding variables

View file

@ -0,0 +1,195 @@
# Stripe Subscription Testing Checklist
## 🔧 Setup für Tests
### 1. Test-Umgebung
- [x] Dev Server läuft (`npm run dev`)
- [ ] Test-Keys sind eingetragen (nicht Live-Keys!)
- [ ] Stripe CLI installiert: `brew install stripe/stripe-cli/stripe`
- [ ] Webhook listening: `stripe listen --forward-to localhost:5173/api/stripe/webhook`
### 2. Test User anlegen
```bash
# In PocketBase Admin (http://localhost:8090/_/)
1. Users Collection öffnen
2. Neuen User erstellen:
- Email: test@example.com
- Password: testtest
- subscription_status: "free"
- links_count: 0
```
## 💳 1. Checkout Flow testen
### Monthly Subscription
- [ ] Auf `/pricing` gehen
- [ ] "Monthly" Button klicken
- [ ] Stripe Checkout öffnet sich
- [ ] Korrekter Preis: €4.99/month
- [ ] Test-Karte: `4242 4242 4242 4242`
- [ ] Payment erfolgreich
### Yearly Subscription
- [ ] "Yearly" Button testen
- [ ] Korrekter Preis: €39.99/year
- [ ] Payment erfolgreich
### Lifetime Payment
- [ ] "Lifetime" Button testen
- [ ] Korrekter Preis: €129.99 (one-time)
- [ ] Payment erfolgreich
## 🔗 2. Webhook Testing
### Manuell mit Stripe CLI
```bash
# Webhook Event simulieren
stripe trigger checkout.session.completed
# Logs checken:
# 1. Terminal wo stripe listen läuft
# 2. Browser Konsole
# 3. PocketBase für User-Update
```
### Automatisch nach echtem Payment
- [ ] Payment durchführen
- [ ] Webhook wird automatisch ausgelöst
- [ ] User Status wird auf "pro" gesetzt
- [ ] stripe_customer_id wird gespeichert
## 👤 3. User Status Testing
### Free User Limits
```bash
# Als Free User einloggen
# Versuche 11 Links zu erstellen
```
- [ ] Link 1-10: Erfolgreich erstellt
- [ ] Link 11: Error "Limit erreicht"
- [ ] Upgrade-Button wird angezeigt
### Pro User Unlimited
- [ ] User auf "pro" setzen (manuell in PocketBase)
- [ ] Beliebig viele Links erstellen können
- [ ] Kein Limit-Check
## 🎛️ 4. Subscription Management
### Status Check API
```bash
curl http://localhost:5173/api/user/subscription
```
- [ ] Gibt aktuellen Status zurück
- [ ] Zeigt nächste Zahlung an (falls recurring)
### Cancel Subscription
- [ ] Stripe Customer Portal Link funktioniert
- [ ] Subscription kann gekündigt werden
- [ ] User Status wird entsprechend geupdatet
## 🧪 5. Edge Cases testen
### Doppelte Subscription
- [ ] Pro User versucht erneut zu upgraden
- [ ] Error: "Du hast bereits ein aktives Abo"
### Abgebrochene Payments
- [ ] Payment abbrechen im Stripe Checkout
- [ ] User bleibt auf "free" Status
- [ ] Keine Änderungen in PocketBase
### Webhook Failures
- [ ] Webhook URL temporär down
- [ ] Stripe retries automatisch
- [ ] Manual retry über Stripe Dashboard
## 📊 6. Integration Testing
### Frontend Updates
- [ ] Navigation zeigt Pro-Status
- [ ] Upgrade-Buttons verschwinden für Pro User
- [ ] Richtige Limits werden angezeigt
### API Protection
```bash
# Als Free User mit 10 Links
curl -X POST http://localhost:5173/api/links \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
```
- [ ] 403 Error bei Limit überschritten
- [ ] Pro User kann unbegrenzt erstellen
## 🎯 7. Test Cards für verschiedene Szenarien
### Erfolgreiche Payments
- `4242 4242 4242 4242` - Visa
- `5555 5555 5555 4444` - Mastercard
- `4000 0025 0000 3155` - Visa (requires authentication)
### Failed Payments
- `4000 0000 0000 0002` - Card declined
- `4000 0000 0000 9995` - Insufficient funds
### SEPA Testing
- IBAN: `DE89370400440532013000`
- [ ] SEPA Direct Debit funktioniert
## ✅ Completion Checklist
- [ ] Alle Checkout-Flows funktionieren
- [ ] Webhooks verarbeiten Events korrekt
- [ ] User Status Updates funktionieren
- [ ] Link-Limits werden enforced
- [ ] Pro Features sind freigeschaltet
- [ ] Error Handling funktioniert
- [ ] Frontend zeigt korrekten Status
## 🚨 Häufige Probleme
### Webhook nicht erhalten
```bash
# Check Stripe CLI Output
stripe listen --forward-to localhost:5173/api/stripe/webhook --log-level debug
```
### User Status nicht geupdated
```sql
-- In PocketBase Admin Console
SELECT * FROM users WHERE email = 'test@example.com';
```
### Payment fehlgeschlagen
- API Keys überprüfen (Test vs Live)
- Price IDs korrekt?
- Webhook Endpoint erreichbar?
---
**Tipp**: Teste immer mit Test-Daten und Test-Keys. Niemals echte Payments in der Entwicklung!