DEV Community

kanta13jp1
kanta13jp1

Posted on

Indie Dev SaaS Pricing Experiments — A/B Testing, Anchoring, and Freemium Strategy

Indie Dev SaaS Pricing Experiments — A/B Testing, Anchoring, and Freemium Strategy

Pricing is as powerful a growth lever as building features. Here's what actually worked.

Core Pricing Experiment Schema

-- Pricing plans table
CREATE TABLE pricing_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,           -- 'starter', 'pro', 'team'
  price_monthly INTEGER NOT NULL, -- cents / yen
  price_yearly INTEGER,
  features JSONB NOT NULL DEFAULT '[]',
  is_active BOOLEAN DEFAULT true,
  variant TEXT DEFAULT 'control' -- for A/B testing
);

-- Track which variant each user saw
CREATE TABLE pricing_impressions (
  user_id UUID REFERENCES profiles(id),
  variant TEXT NOT NULL,
  converted BOOLEAN DEFAULT false,
  seen_at TIMESTAMPTZ DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Anchoring: Show the Expensive Plan First

// Display pricing cards high → low order
final plans = [
  PricingPlan(name: 'Team', price: 9800, highlight: false),
  PricingPlan(name: 'Pro', price: 2980, highlight: true),   // recommended
  PricingPlan(name: 'Starter', price: 980, highlight: false),
];

// Showing Team plan first makes Pro look like a deal
Widget _buildPricingCards() {
  return Row(
    children: plans.map((p) => PricingCard(plan: p)).toList(),
  );
}
Enter fullscreen mode Exit fullscreen mode

Freemium → Paid Conversion Trigger

// Edge Function: usage check + upgrade nudge
const LIMITS = { free: { projects: 3, members: 1 } };

export async function checkUsageLimit(userId: string, resource: string) {
  const { count } = await supabase
    .from(resource)
    .select('*', { count: 'exact', head: true })
    .eq('user_id', userId);

  const plan = await getUserPlan(userId);
  const limit = LIMITS[plan]?.[resource];

  if (limit && count >= limit) {
    return { blocked: true, upgradeUrl: '/pricing?ref=limit' };
  }
  return { blocked: false };
}
Enter fullscreen mode Exit fullscreen mode

Annual Discount Psychology

// Monthly vs annual comparison display
String get yearlyDiscount {
  final monthly = priceMonthly * 12;
  final yearly = priceYearly ?? monthly;
  final discount = ((monthly - yearly) / monthly * 100).round();
  return '$discount% OFF';
}

// "X months free" framing outperforms "Y% off"
String get freeMonthsText {
  final monthly = priceMonthly;
  final savings = (priceMonthly * 12) - (priceYearly ?? 0);
  final freeMonths = (savings / monthly).floor();
  return '$freeMonths months free';
}
Enter fullscreen mode Exit fullscreen mode

Summary

Anchoring         → Show high-tier first; middle tier looks affordable
Freemium trigger  → Block at usage limit + ref= param for conversion tracking
Annual discount   → "X months free" framing > percentage discount
A/B testing       → pricing_impressions table tracks variant → conversion rate
Enter fullscreen mode Exit fullscreen mode

Pricing isn't a one-time decision — treat it as a product feature with continuous hypothesis → measure → iterate cycles.

Top comments (0)