Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
612 lines
20 KiB
Markdown
612 lines
20 KiB
Markdown
# SaaS Pricing Strategies
|
||
|
||
**Meta-Description:** SaaS Pricing Modelle implementieren. Freemium, Tiered Pricing, Usage-Based und Value-Based Pricing mit Stripe Integration.
|
||
|
||
**Keywords:** SaaS Pricing, Freemium, Tiered Pricing, Usage-Based, Value Pricing, Stripe, Subscription, Revenue Model
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
**Pricing** ist einer der wichtigsten Hebel für SaaS-Wachstum. Das richtige **Pricing-Modell** kann Revenue verdoppeln ohne zusätzliche Kunden. Dieser Guide zeigt verschiedene Strategien und deren technische Implementation.
|
||
|
||
---
|
||
|
||
## Pricing Models Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SAAS PRICING MODELS │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. FLAT-RATE PRICING: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ $49/month - All features included │ │
|
||
│ │ ✓ Simple to understand │ │
|
||
│ │ ✗ Limited upsell potential │ │
|
||
│ │ Example: Basecamp │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 2. TIERED PRICING: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Starter Pro Enterprise │ │
|
||
│ │ $19/mo $49/mo $199/mo │ │
|
||
│ │ 5 users 25 users Unlimited │ │
|
||
│ │ ✓ Clear upgrade path │ │
|
||
│ │ ✓ Captures different segments │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 3. USAGE-BASED (Pay-as-you-go): │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ $0.01 per API call / $0.10 per GB │ │
|
||
│ │ ✓ Low barrier to entry │ │
|
||
│ │ ✓ Revenue scales with customer success │ │
|
||
│ │ ✗ Unpredictable revenue │ │
|
||
│ │ Example: AWS, Twilio │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 4. FREEMIUM: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Free tier + Premium tiers │ │
|
||
│ │ ✓ Viral growth potential │ │
|
||
│ │ ✓ Low friction acquisition │ │
|
||
│ │ ✗ High support costs │ │
|
||
│ │ Example: Slack, Dropbox │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 5. PER-SEAT PRICING: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ $10/user/month │ │
|
||
│ │ ✓ Predictable, easy to understand │ │
|
||
│ │ ✓ Grows with customer's team │ │
|
||
│ │ Example: Notion, Linear │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Pricing Configuration
|
||
|
||
```typescript
|
||
// lib/pricing/config.ts
|
||
export interface PricingTier {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
monthlyPrice: number;
|
||
yearlyPrice: number;
|
||
features: Feature[];
|
||
limits: TierLimits;
|
||
highlighted?: boolean;
|
||
ctaText: string;
|
||
stripeMonthlyPriceId: string;
|
||
stripeYearlyPriceId: string;
|
||
}
|
||
|
||
export interface Feature {
|
||
name: string;
|
||
included: boolean;
|
||
limit?: string;
|
||
tooltip?: string;
|
||
}
|
||
|
||
export interface TierLimits {
|
||
users: number;
|
||
projects: number;
|
||
storage: number; // in GB
|
||
apiCalls: number;
|
||
customDomains: number;
|
||
support: 'community' | 'email' | 'priority' | 'dedicated';
|
||
}
|
||
|
||
export const pricingTiers: PricingTier[] = [
|
||
{
|
||
id: 'free',
|
||
name: 'Free',
|
||
description: 'For individuals getting started',
|
||
monthlyPrice: 0,
|
||
yearlyPrice: 0,
|
||
features: [
|
||
{ name: 'Up to 3 projects', included: true },
|
||
{ name: 'Basic analytics', included: true },
|
||
{ name: 'Community support', included: true },
|
||
{ name: 'API access', included: false },
|
||
{ name: 'Custom domains', included: false },
|
||
{ name: 'Team collaboration', included: false }
|
||
],
|
||
limits: {
|
||
users: 1,
|
||
projects: 3,
|
||
storage: 1,
|
||
apiCalls: 1000,
|
||
customDomains: 0,
|
||
support: 'community'
|
||
},
|
||
ctaText: 'Get Started Free',
|
||
stripeMonthlyPriceId: '',
|
||
stripeYearlyPriceId: ''
|
||
},
|
||
{
|
||
id: 'pro',
|
||
name: 'Pro',
|
||
description: 'For professionals and small teams',
|
||
monthlyPrice: 29,
|
||
yearlyPrice: 290, // 2 months free
|
||
features: [
|
||
{ name: 'Unlimited projects', included: true },
|
||
{ name: 'Advanced analytics', included: true },
|
||
{ name: 'Email support', included: true },
|
||
{ name: 'API access', included: true, limit: '10k calls/mo' },
|
||
{ name: 'Custom domains', included: true, limit: '3 domains' },
|
||
{ name: 'Team collaboration', included: true, limit: '5 members' }
|
||
],
|
||
limits: {
|
||
users: 5,
|
||
projects: -1, // unlimited
|
||
storage: 50,
|
||
apiCalls: 10000,
|
||
customDomains: 3,
|
||
support: 'email'
|
||
},
|
||
highlighted: true,
|
||
ctaText: 'Start Free Trial',
|
||
stripeMonthlyPriceId: 'price_pro_monthly',
|
||
stripeYearlyPriceId: 'price_pro_yearly'
|
||
},
|
||
{
|
||
id: 'enterprise',
|
||
name: 'Enterprise',
|
||
description: 'For large teams with advanced needs',
|
||
monthlyPrice: 99,
|
||
yearlyPrice: 990,
|
||
features: [
|
||
{ name: 'Unlimited everything', included: true },
|
||
{ name: 'Custom analytics', included: true },
|
||
{ name: 'Dedicated support', included: true },
|
||
{ name: 'API access', included: true, limit: 'Unlimited' },
|
||
{ name: 'Custom domains', included: true, limit: 'Unlimited' },
|
||
{ name: 'Team collaboration', included: true, limit: 'Unlimited' },
|
||
{ name: 'SSO/SAML', included: true },
|
||
{ name: 'SLA guarantee', included: true }
|
||
],
|
||
limits: {
|
||
users: -1,
|
||
projects: -1,
|
||
storage: -1,
|
||
apiCalls: -1,
|
||
customDomains: -1,
|
||
support: 'dedicated'
|
||
},
|
||
ctaText: 'Contact Sales',
|
||
stripeMonthlyPriceId: 'price_enterprise_monthly',
|
||
stripeYearlyPriceId: 'price_enterprise_yearly'
|
||
}
|
||
];
|
||
|
||
// Check if feature is available
|
||
export function hasFeature(
|
||
tierId: string,
|
||
featureName: string
|
||
): boolean {
|
||
const tier = pricingTiers.find(t => t.id === tierId);
|
||
if (!tier) return false;
|
||
|
||
const feature = tier.features.find(f => f.name === featureName);
|
||
return feature?.included ?? false;
|
||
}
|
||
|
||
// Check limits
|
||
export function checkLimit(
|
||
tierId: string,
|
||
limitKey: keyof TierLimits,
|
||
currentUsage: number
|
||
): { allowed: boolean; limit: number; usage: number } {
|
||
const tier = pricingTiers.find(t => t.id === tierId);
|
||
if (!tier) return { allowed: false, limit: 0, usage: currentUsage };
|
||
|
||
const limit = tier.limits[limitKey];
|
||
|
||
return {
|
||
allowed: limit === -1 || currentUsage < limit,
|
||
limit,
|
||
usage: currentUsage
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Pricing Component
|
||
|
||
```tsx
|
||
// components/pricing/PricingTable.tsx
|
||
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { pricingTiers, PricingTier } from '@/lib/pricing/config';
|
||
import { CheckIcon, XIcon } from 'lucide-react';
|
||
import { track } from '@/lib/analytics';
|
||
|
||
export function PricingTable() {
|
||
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>('monthly');
|
||
|
||
const handleToggle = () => {
|
||
const newPeriod = billingPeriod === 'monthly' ? 'yearly' : 'monthly';
|
||
setBillingPeriod(newPeriod);
|
||
track('pricing_period_changed', { period: newPeriod });
|
||
};
|
||
|
||
return (
|
||
<section className="py-20">
|
||
<div className="container mx-auto px-4">
|
||
{/* Header */}
|
||
<div className="text-center mb-12">
|
||
<h2 className="text-4xl font-bold mb-4">
|
||
Simple, transparent pricing
|
||
</h2>
|
||
<p className="text-xl text-gray-600">
|
||
No hidden fees. Cancel anytime.
|
||
</p>
|
||
|
||
{/* Billing Toggle */}
|
||
<div className="flex items-center justify-center gap-4 mt-8">
|
||
<span className={billingPeriod === 'monthly' ? 'font-semibold' : 'text-gray-500'}>
|
||
Monthly
|
||
</span>
|
||
<button
|
||
onClick={handleToggle}
|
||
className="relative w-14 h-7 bg-gray-200 rounded-full transition"
|
||
aria-label="Toggle billing period"
|
||
>
|
||
<span
|
||
className={`absolute w-5 h-5 bg-blue-600 rounded-full top-1 transition-all ${
|
||
billingPeriod === 'yearly' ? 'left-8' : 'left-1'
|
||
}`}
|
||
/>
|
||
</button>
|
||
<span className={billingPeriod === 'yearly' ? 'font-semibold' : 'text-gray-500'}>
|
||
Yearly
|
||
<span className="ml-2 text-green-600 text-sm">Save 17%</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pricing Cards */}
|
||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||
{pricingTiers.map((tier) => (
|
||
<PricingCard
|
||
key={tier.id}
|
||
tier={tier}
|
||
billingPeriod={billingPeriod}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* FAQ Link */}
|
||
<div className="text-center mt-12">
|
||
<p className="text-gray-600">
|
||
Have questions?{' '}
|
||
<a href="/faq" className="text-blue-600 hover:underline">
|
||
Check our FAQ
|
||
</a>{' '}
|
||
or{' '}
|
||
<a href="/contact" className="text-blue-600 hover:underline">
|
||
contact us
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function PricingCard({
|
||
tier,
|
||
billingPeriod
|
||
}: {
|
||
tier: PricingTier;
|
||
billingPeriod: 'monthly' | 'yearly';
|
||
}) {
|
||
const price = billingPeriod === 'monthly' ? tier.monthlyPrice : tier.yearlyPrice;
|
||
const pricePerMonth = billingPeriod === 'yearly' ? Math.round(price / 12) : price;
|
||
|
||
const handleCTAClick = () => {
|
||
track('pricing_cta_clicked', {
|
||
tier: tier.id,
|
||
billingPeriod,
|
||
price
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`relative rounded-2xl p-8 ${
|
||
tier.highlighted
|
||
? 'bg-blue-600 text-white ring-4 ring-blue-600 ring-offset-2'
|
||
: 'bg-white border border-gray-200'
|
||
}`}
|
||
>
|
||
{tier.highlighted && (
|
||
<span className="absolute -top-4 left-1/2 -translate-x-1/2 bg-yellow-400 text-yellow-900 px-4 py-1 rounded-full text-sm font-semibold">
|
||
Most Popular
|
||
</span>
|
||
)}
|
||
|
||
<h3 className="text-2xl font-bold">{tier.name}</h3>
|
||
<p className={`mt-2 ${tier.highlighted ? 'text-blue-100' : 'text-gray-600'}`}>
|
||
{tier.description}
|
||
</p>
|
||
|
||
{/* Price */}
|
||
<div className="mt-6">
|
||
<span className="text-5xl font-bold">
|
||
${pricePerMonth}
|
||
</span>
|
||
<span className={tier.highlighted ? 'text-blue-100' : 'text-gray-500'}>
|
||
/month
|
||
</span>
|
||
{billingPeriod === 'yearly' && price > 0 && (
|
||
<p className={`text-sm mt-1 ${tier.highlighted ? 'text-blue-100' : 'text-gray-500'}`}>
|
||
Billed ${price} annually
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* CTA */}
|
||
<button
|
||
onClick={handleCTAClick}
|
||
className={`w-full mt-8 py-3 rounded-lg font-semibold transition ${
|
||
tier.highlighted
|
||
? 'bg-white text-blue-600 hover:bg-blue-50'
|
||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||
}`}
|
||
>
|
||
{tier.ctaText}
|
||
</button>
|
||
|
||
{/* Features */}
|
||
<ul className="mt-8 space-y-4">
|
||
{tier.features.map((feature, index) => (
|
||
<li key={index} className="flex items-start gap-3">
|
||
{feature.included ? (
|
||
<CheckIcon className={`w-5 h-5 flex-shrink-0 ${
|
||
tier.highlighted ? 'text-blue-200' : 'text-green-500'
|
||
}`} />
|
||
) : (
|
||
<XIcon className="w-5 h-5 flex-shrink-0 text-gray-400" />
|
||
)}
|
||
<span className={!feature.included ? 'text-gray-400' : ''}>
|
||
{feature.name}
|
||
{feature.limit && (
|
||
<span className={`text-sm ${tier.highlighted ? 'text-blue-200' : 'text-gray-500'}`}>
|
||
{' '}({feature.limit})
|
||
</span>
|
||
)}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Usage-Based Pricing
|
||
|
||
```typescript
|
||
// lib/pricing/usage.ts
|
||
export interface UsageMetric {
|
||
id: string;
|
||
name: string;
|
||
unit: string;
|
||
pricePerUnit: number;
|
||
freeAllowance: number;
|
||
tiers?: UsageTier[];
|
||
}
|
||
|
||
export interface UsageTier {
|
||
upTo: number;
|
||
pricePerUnit: number;
|
||
}
|
||
|
||
export const usageMetrics: UsageMetric[] = [
|
||
{
|
||
id: 'api_calls',
|
||
name: 'API Calls',
|
||
unit: 'request',
|
||
pricePerUnit: 0.0001, // $0.0001 per call
|
||
freeAllowance: 10000,
|
||
tiers: [
|
||
{ upTo: 100000, pricePerUnit: 0.0001 },
|
||
{ upTo: 1000000, pricePerUnit: 0.00008 },
|
||
{ upTo: Infinity, pricePerUnit: 0.00005 }
|
||
]
|
||
},
|
||
{
|
||
id: 'storage',
|
||
name: 'Storage',
|
||
unit: 'GB',
|
||
pricePerUnit: 0.10,
|
||
freeAllowance: 5,
|
||
tiers: [
|
||
{ upTo: 100, pricePerUnit: 0.10 },
|
||
{ upTo: 1000, pricePerUnit: 0.08 },
|
||
{ upTo: Infinity, pricePerUnit: 0.05 }
|
||
]
|
||
},
|
||
{
|
||
id: 'bandwidth',
|
||
name: 'Bandwidth',
|
||
unit: 'GB',
|
||
pricePerUnit: 0.05,
|
||
freeAllowance: 10
|
||
}
|
||
];
|
||
|
||
export function calculateUsageCost(
|
||
metricId: string,
|
||
usage: number
|
||
): number {
|
||
const metric = usageMetrics.find(m => m.id === metricId);
|
||
if (!metric) return 0;
|
||
|
||
// Subtract free allowance
|
||
const billableUsage = Math.max(0, usage - metric.freeAllowance);
|
||
if (billableUsage === 0) return 0;
|
||
|
||
// Calculate with tiers if available
|
||
if (metric.tiers) {
|
||
let remaining = billableUsage;
|
||
let cost = 0;
|
||
let previousLimit = 0;
|
||
|
||
for (const tier of metric.tiers) {
|
||
const tierUsage = Math.min(remaining, tier.upTo - previousLimit);
|
||
cost += tierUsage * tier.pricePerUnit;
|
||
remaining -= tierUsage;
|
||
previousLimit = tier.upTo;
|
||
|
||
if (remaining <= 0) break;
|
||
}
|
||
|
||
return cost;
|
||
}
|
||
|
||
return billableUsage * metric.pricePerUnit;
|
||
}
|
||
|
||
export function calculateTotalUsageCost(
|
||
usage: Record<string, number>
|
||
): { breakdown: Record<string, number>; total: number } {
|
||
const breakdown: Record<string, number> = {};
|
||
let total = 0;
|
||
|
||
for (const [metricId, amount] of Object.entries(usage)) {
|
||
const cost = calculateUsageCost(metricId, amount);
|
||
breakdown[metricId] = cost;
|
||
total += cost;
|
||
}
|
||
|
||
return { breakdown, total };
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Stripe Integration
|
||
|
||
```typescript
|
||
// lib/stripe/checkout.ts
|
||
import Stripe from 'stripe';
|
||
|
||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||
apiVersion: '2024-06-20'
|
||
});
|
||
|
||
export async function createCheckoutSession(
|
||
customerId: string,
|
||
priceId: string,
|
||
options?: {
|
||
trialDays?: number;
|
||
successUrl?: string;
|
||
cancelUrl?: string;
|
||
couponId?: string;
|
||
}
|
||
): Promise<string> {
|
||
const session = await stripe.checkout.sessions.create({
|
||
customer: customerId,
|
||
mode: 'subscription',
|
||
payment_method_types: ['card'],
|
||
line_items: [
|
||
{
|
||
price: priceId,
|
||
quantity: 1
|
||
}
|
||
],
|
||
subscription_data: {
|
||
trial_period_days: options?.trialDays || 14,
|
||
...(options?.couponId && {
|
||
coupon: options.couponId
|
||
})
|
||
},
|
||
success_url: options?.successUrl || `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
||
cancel_url: options?.cancelUrl || `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
|
||
allow_promotion_codes: true
|
||
});
|
||
|
||
return session.url!;
|
||
}
|
||
|
||
export async function createBillingPortalSession(
|
||
customerId: string
|
||
): Promise<string> {
|
||
const session = await stripe.billingPortal.sessions.create({
|
||
customer: customerId,
|
||
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`
|
||
});
|
||
|
||
return session.url;
|
||
}
|
||
|
||
// Usage-based billing
|
||
export async function reportUsage(
|
||
subscriptionItemId: string,
|
||
quantity: number,
|
||
timestamp?: number
|
||
): Promise<void> {
|
||
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
|
||
quantity,
|
||
timestamp: timestamp || Math.floor(Date.now() / 1000),
|
||
action: 'increment'
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Pricing Psychology
|
||
|
||
| Tactic | Implementation |
|
||
|--------|---------------|
|
||
| **Anchoring** | Show highest tier first or middle highlighted |
|
||
| **Decoy Effect** | Add a tier that makes others look better |
|
||
| **Charm Pricing** | $29 instead of $30 |
|
||
| **Annual Discount** | 2 months free for yearly |
|
||
| **Social Proof** | "Most Popular" badge |
|
||
| **Loss Aversion** | "Limited time offer" |
|
||
| **Value Framing** | Compare to alternatives |
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
SaaS Pricing Strategien erfordern:
|
||
|
||
1. **Know Your Customers**: Segmente verstehen
|
||
2. **Value-Based**: Preis an Kundennutzen koppeln
|
||
3. **Test & Iterate**: A/B Testing für Pricing
|
||
4. **Clear Communication**: Transparente Preisgestaltung
|
||
|
||
Pricing ist nie "fertig" – kontinuierlich optimieren.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "SaaS pricing page design, three-tier comparison with highlighted option"
|
||
2. "Usage-based pricing calculator interface, slider and cost preview"
|
||
3. "Pricing psychology infographic, anchoring and decoy effects"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [Stripe Pricing Guide](https://stripe.com/guides/atlas/saas-pricing)
|
||
- [ProfitWell Pricing Strategy](https://www.profitwell.com/recur/all/pricing-strategy)
|
||
- [OpenView Pricing](https://openviewpartners.com/blog/saas-pricing-models/)
|
||
- [Price Intelligently](https://www.priceintelligently.com/)
|