DEV Community

kanta13jp1
kanta13jp1

Posted on

Indie SaaS Monetization — Hard Paywall vs Freemium vs Usage-Based Pricing

Indie SaaS Monetization — Hard Paywall vs Freemium vs Usage-Based Pricing

The hardest decision in indie SaaS isn't building the product — it's deciding where to put the wall. Too low and nobody converts. Too high and nobody signs up. Here are three patterns with real implementation examples.

Pattern 1: Hard Paywall (Action Limit)

Lock users out after N actions. Simple, predictable revenue.

CREATE TABLE usage_limits (
  user_id UUID PRIMARY KEY REFERENCES auth.users(id),
  action_count INTEGER DEFAULT 0,
  reset_at TIMESTAMPTZ DEFAULT date_trunc('month', NOW()) + INTERVAL '1 month',
  plan TEXT DEFAULT 'free'
);

CREATE OR REPLACE FUNCTION increment_usage(p_user_id UUID, p_limit INT)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
  current_count INTEGER;
  user_plan TEXT;
BEGIN
  SELECT action_count, plan INTO current_count, user_plan
  FROM usage_limits WHERE user_id = p_user_id;

  IF user_plan = 'pro' THEN RETURN TRUE; END IF;
  IF current_count >= p_limit THEN RETURN FALSE; END IF;

  UPDATE usage_limits SET action_count = action_count + 1
  WHERE user_id = p_user_id;

  RETURN TRUE;
END;
$$;
Enter fullscreen mode Exit fullscreen mode
Future<bool> checkAndIncrementUsage() async {
  final allowed = await supabase.rpc('increment_usage', params: {
    'p_user_id': supabase.auth.currentUser!.id,
    'p_limit': 10,
  }) as bool;

  if (!allowed) _showUpgradeDialog();
  return allowed;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Freemium (Feature Gate)

Core free, advanced features paid. Best for user acquisition.

enum Plan { free, pro }

class FeatureGate extends StatelessWidget {
  final Plan requiredPlan;
  final Widget child;
  final Widget? fallback;

  const FeatureGate({
    required this.requiredPlan,
    required this.child,
    this.fallback,
  });

  @override
  Widget build(BuildContext context) {
    final plan = context.read<UserProvider>().plan;
    if (plan.index >= requiredPlan.index) return child;
    return fallback ?? UpgradePromptWidget(requiredPlan: requiredPlan);
  }
}

// Usage
FeatureGate(
  requiredPlan: Plan.pro,
  child: AIAnalysisButton(),
  fallback: ProTeaser(title: 'AI Analysis', description: 'Unlimited on Pro'),
)
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Usage-Based (Credits)

Align price with value. Best for AI features where your cost scales with usage.

CREATE TABLE user_credits (
  user_id UUID PRIMARY KEY,
  credits INTEGER DEFAULT 100,
  total_used INTEGER DEFAULT 0
);

CREATE OR REPLACE FUNCTION consume_credits(p_user_id UUID, p_amount INTEGER)
RETURNS JSON
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE current_credits INTEGER;
BEGIN
  SELECT credits INTO current_credits
  FROM user_credits WHERE user_id = p_user_id;

  IF current_credits < p_amount THEN
    RETURN json_build_object('success', false, 'reason', 'insufficient_credits');
  END IF;

  UPDATE user_credits
  SET credits = credits - p_amount, total_used = total_used + p_amount
  WHERE user_id = p_user_id;

  RETURN json_build_object('success', true, 'remaining', current_credits - p_amount);
END;
$$;
Enter fullscreen mode Exit fullscreen mode

Payment Provider Comparison

Provider Indie-friendly Tax handling Notes
Stripe Manual Most features; official Supabase integration
RevenueCat Platform handles Best for in-app purchases
Lemon Squeezy MoR (automatic) Easiest for solo devs
Paddle MoR (automatic) Good EU VAT support

My Hybrid Approach

In my app (自分株式会社), I use freemium + usage-based:

  • Free: core features + 100 AI credits/month
  • Pro ¥980/month: unlimited AI + advanced analytics + data export

Conversion rate: ~3.2% (industry average 2-5%). The free tier is generous enough for users to see real value before deciding to pay.


What monetization model works for your indie app? I'd love to hear what conversion rates you're seeing.

Top comments (0)