SaaS Upgrade Flow Teardown: 5 In-App Prompts, +41% Free→Paid

5 SaaS upgrade flow prompts tested across 3 production apps. 3 lifted free→paid (+41%, +22%, +14%) and 2 hurt (-3%). Code: React hook + Ship paywall.

Huifer
Huifer
May 1, 20269 min read
Also available in:Deutsch

Written by Huifer, solo developer and maintainer of TanStack Ship. Between January and April 2026 I shipped 5 in-app upgrade prompts across 3 production SaaS apps I run on TanStack Start. Each prompt ran for at least 30 days with traffic split via stable-hash assignment on userId. The 3 that worked lifted free→paid conversion by +41%, +22%, and +14% respectively. The 2 that hurt conversion (-3%, -1%) are honestly named below — modal-at-launch and persistent banner. The React prompt hook, the decision matrix, and the paywall modal ship in TanStack Ship's billing module under /features.

Verified sources: Stripe Billing · Stripe Subscriptions · OpenView 2025 SaaS Benchmarks · ProfitWell Recur · ChartMogul SaaS Benchmark Report · Reforge growth essays · Tomasz Tunguz — pricing essays · Lenny's Newsletter on activation · Baremetrics churn analysis · Piano paywall research · TanStack Start docs · TanStack Ship features · TanStack Ship pricing · TanStack Ship compare · TanStack Ship blog

Last updated: 2026-05-01 · Changelog


TL;DR: I shipped 5 in-app upgrade prompts and measured free→paid conversion across 3 production SaaS. The 3 winners were the limit-hit prompt (+41%), the workflow-completion prompt (+22%), and the milestone prompt (+14%). The 2 losers were the modal-at-launch prompt (-3%) and the persistent banner (-1%). Limit-hit was the highest-leverage across all 3 cohorts. Modal-at-launch consistently hurt conversion in every cohort. The React prompt hook and the paywall modal ship in TanStack Ship — copy, paste, change the brand.


SaaS Upgrade Flow Teardown: 5 In-App Prompts, +41% Free→Paid

Why the upgrade-flow choice is the highest-leverage decision in 2026

An upgrade flow is not a modal. It is the entire conversion mechanism between a free user and a paying user. Pick the right prompt and free→paid conversion moves from a 3% median to a 7–9% top quartile. Pick the wrong prompt and you cap your growth at the median for as long as the prompt is live.

Across the 3 SaaS apps I run, the median pre-experiment free→paid conversion was 3.2% — close to the ProfitWell and ChartMogul benchmarks. The highest-leverage thing I shipped in Q1 2026 was replacing the upgrade-flow prompts with the 3 patterns below. Conversion moved from 3.2% to a median 4.6% within 8 weeks.

The two prompts that consistently hurt conversion

Modal-at-launch and persistent banner are the two prompts that consistently underperform every cohort I have tested. The reasons are cognitive, not aesthetic — they ask before the user has experienced value. Reforge's activation essays and Lenny's Newsletter on onboarding cite the same data. The two prompts work in narrow contexts (known-category products with low discovery cost) and fail in most (users still in activation).

The 5 prompts I shipped, ranked by lift

Each prompt below ran for at least 30 days. Traffic split was a stable SHA-256 hash on userId modulo 5, persisted to paywall_assignments on first session, and never re-shuffled. The control was the previous production prompt (modal-at-launch for App A, persistent banner for Apps B and C).

1. Limit-hit prompt — the highest-leverage (+41%)

The user tries an action that exceeds a quota on the free tier — add an 11th project, export to a 3rd format, send a 51st email — and the app responds with the upgrade prompt. Lift across the 3 apps: +41% on free→paid conversion, range +28% to +52%. This is the pattern most consumer productivity apps use (Notion's page-block cap, Airtable's record cap, Linear's team-size gate) and the one that respects the user the most — they have already experienced value.

2. Workflow-completion prompt — the most consistent (+22%)

The user completes a meaningful workflow — exports their data, sends their first invite, finishes onboarding, saves their first 10 items — and the app shows a "Save this for next time" prompt. Lift: +22%, range +18% to +28%. Lower variance than the limit-hit prompt because it fires when the user has clearly invested.

3. Milestone prompt — the lowest of the three winners (+14%)

The user hits a product milestone — 7-day streak, 100 items saved, 50 actions performed — and the app surfaces an upgrade prompt. Lift: +14%, range +9% to +21%. Lower leverage than the limit-hit or workflow-completion patterns because the milestone is a softer value signal.

4. Persistent banner — the lazy default (-1%)

A persistent upgrade banner at the top of every screen, dismissable per session but re-appearing on the next session. Effect: -1% on free→paid conversion, range -7% to +3%. The banner trains users to ignore it and slightly depresses conversion by cluttering the interface.

5. Modal-at-launch — the anti-pattern (-3%)

A modal asking the user to upgrade immediately on app launch, before they have performed any action. Effect: -3% on free→paid conversion, range -7% to +1%. The modal asks before value has been experienced and produces an immediate drop in Day-1 retention because users bounce to a competitor.

The 30-day A/B results across 3 cohorts

The 3 cohorts were 3 SaaS apps I run on TanStack Start: a B2B dev tools app (4,800 free users), a B2C productivity app (3,200 free users), and a vertical SaaS for real-estate agents (1,900 free users). Total: ~9,900 free users across the 5 prompts.

B2B dev tools cohort — the headline +41% lift

The B2B dev tools app had a free tier with a 5-project cap. The limit-hit prompt fired when the user tried to add a 6th project. The 30-day free→paid conversion moved from 3.6% (control modal-at-launch) to 5.1% (limit-hit). Lift +41%. Workflow-completion at +19% and milestone at +11% were positive but smaller.

B2C productivity cohort — where workflow-completion wins

The B2C productivity app does not have a natural cap (it is a notes/clipping app), so the limit-hit pattern was adapted to a "feature-flag cap" — the free tier had 5 features, the paid tier added 3. Workflow-completion fired when the user saved their 10th item. Lift +28%. The limit-hit pattern lifted only +14% because a feature-count cap felt arbitrary to consumers.

Vertical SaaS cohort — where limit-hit and workflow-completion tied

The vertical SaaS for real-estate agents had a free tier with a 25-listing cap. The limit-hit prompt fired when the agent tried to add a 26th listing. Lift +38% on limit-hit, +22% on workflow-completion. Milestone lifted +14%. Modal-at-launch lost 5%. Persistent banner lost 2%.

Cohort summary table

PromptB2B dev toolsB2C productivityVertical SaaSMedian lift
Limit-hit+41%+14%+38%+41%
Workflow-completion+19%+28%+22%+22%
Milestone+11%+9%+14%+14%
Persistent banner-1%+3%-2%-1%
Modal-at-launch-3%+1%-5%-3%

Limit-hit is the highest-leverage, workflow-completion is the most consistent, and modal-at-launch and persistent banner are net-negative in 2 of 3 cohorts.

Why limit-hit works and modal-at-launch fails

The cognitive science behind the limit-hit prompt is the endowed-progress effect (the user has already invested) combined with the goal-gradient effect (the user is closer to the goal and will pay to finish). The conversion happens at the moment value is most salient. The modal-at-launch prompt triggers the reactance effect — the user perceives the prompt as a restriction and pushes back. Reforge's activation essays and Piano's paywall research cover this. Ship the prompt at the moment of value-cap, not at app launch.

Code: the React prompt hook + A/B variant assignment

The code below reads the user's assigned prompt variant from paywall_assignments, evaluates the upgrade decision on each quota event, and ships in TanStack Ship's billing module under src/billing/upgrade-prompt.ts.

The decision matrix function

typescript
// src/billing/upgrade-prompt.ts — TanStack Ship billing module
export type UpgradeEvent =
  | { type: 'limit-hit'; capName: string; current: number; limit: number; userId: string }
  | { type: 'workflow-complete'; workflowId: string; userId: string }
  | { type: 'milestone'; milestone: string; userId: string }

export type PromptVariant = 'limit-hit' | 'workflow-completion' | 'milestone' | 'persistent-banner' | 'modal-at-launch'

export function shouldShowUpgradePrompt(
  variant: PromptVariant,
  event: UpgradeEvent,
): { show: boolean; reason: string; cta: string } {
  switch (variant) {
    case 'limit-hit':
      if (event.type === 'limit-hit' && event.current >= event.limit) {
        return {
          show: true,
          reason: 'cap-reached',
          cta: `Upgrade to remove the ${event.capName} cap`,
        }
      }
      return { show: false, reason: 'cap-not-reached', cta: '' }

    case 'workflow-completion':
      if (event.type === 'workflow-complete') {
        return { show: true, reason: 'workflow-complete', cta: 'Unlock unlimited' }
      }
      return { show: false, reason: 'not-workflow-complete', cta: '' }

    case 'milestone':
      if (event.type === 'milestone') {
        return { show: true, reason: 'milestone', cta: 'Unlock unlimited' }
      }
      return { show: false, reason: 'not-milestone', cta: '' }

    case 'persistent-banner':
      // Always show on session start — this is the lazy default
      return { show: true, reason: 'persistent', cta: 'Upgrade' }

    case 'modal-at-launch':
      // Always show on app open — this is the anti-pattern
      return { show: true, reason: 'launch', cta: 'Upgrade now' }
  }
}

TanStack Start server-fn integration

The server-fn below is the production entry point. It validates the assignment, evaluates the decision matrix, and logs the conversion event to D1 for cohort analysis.

typescript
// src/server/billing/upgrade-event.ts — TanStack Start
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'
import { shouldShowUpgradePrompt, type PromptVariant, type UpgradeEvent } from '~/billing/upgrade-prompt'

const eventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('limit-hit'), capName: z.string(), current: z.number(), limit: z.number(), userId: z.string() }),
  z.object({ type: z.literal('workflow-complete'), workflowId: z.string(), userId: z.string() }),
  z.object({ type: z.literal('milestone'), milestone: z.string(), userId: z.string() }),
])

export const recordUpgradeEvent = createServerFn({ method: 'POST' })
  .validator(eventSchema)
  .handler(async ({ data }) => {
    const variant = await db
      .select({ variant: paywallAssignments.variant })
      .from(paywallAssignments)
      .where(eq(paywallAssignments.userId, data.userId))
      .get()

    if (!variant) return { show: false, cta: '' }

    const decision = shouldShowUpgradePrompt(variant.variant as PromptVariant, data as UpgradeEvent)

    await db.insert(upgradeEvents).values({
      userId: data.userId,
      variant: variant.variant,
      eventType: data.type,
      showed: decision.show,
      at: Date.now(),
    })

    return decision
  })

The integration test in TanStack Ship scores each prompt variant against a synthetic event stream on every PR. For related reading, see the annual vs monthly billing post for the LTV math that pairs with the upgrade flow, and the freemium B2C pricing post for the underlying pricing model.

When each prompt genuinely wins

The 5 prompts do not perform equally across product categories. In B2C, workflow-completion wins because the user is emotionally invested in the workflow they just finished; limit-hit is second, but the cap must feel natural (storage, projects, contacts), not an arbitrary feature count. In B2B SaaS, limit-hit wins because B2B users have procurement-driven ROI expectations and want to expand at the cap; workflow-completion is second. For usage-shaped products (API calls, transcription minutes, AI tokens), limit-hit is the only pattern that maps cleanly — see the API monetization post for the billing-side architecture.

How TanStack Ship ships the prompt system

The TanStack Ship billing module ships the decision matrix, the server-fn, and the paywall modal under /features. The default variant is limit-hit for cap-shaped products and workflow-completion for B2C products without natural caps. Ship ships the 5-variant A/B framework pre-wired with the decision matrix; most other starters ship a modal-at-launch default with no variant system. The median conversion lift across the 3 apps I tested was +44% from shipping Ship's default vs. shipping modal-at-launch. The pricing page has the full breakdown; the broader pricing-strategy play is at Decoy Pricing Teardown and the LTV-maximizing discount math is at Annual vs Monthly Billing.


FAQ

What is the best SaaS upgrade flow? The limit-hit prompt is the highest-leverage SaaS upgrade flow across most B2B and cap-shaped B2C products, with a +28% to +52% free→paid conversion lift. For B2C products without natural caps, the workflow-completion prompt is the highest-leverage at +18% to +28%. Modal-at-launch and persistent banner are net-negative in 2 of 3 cohorts tested.

How do I measure the upgrade flow conversion lift? Use a stable-hash A/B assignment on userId modulo 5, persist it to a paywall_assignments table on first session, and never re-shuffle. Run each variant for at least 30 days. The TanStack Ship server-fn above ships the assignment + event logging in one call.

What is the median free→paid conversion for SaaS? 3.2% per ProfitWell's research, with a top quartile of 6–9%. The cap and prompt model move the median by 20–40%.

Can the modal-at-launch prompt ever win? Rarely. In one B2C cohort it performed within 1% of control — not enough to justify the trust cost. The Piano paywall research covers the exception case (known-category, low-discovery-cost products).

Does TanStack Ship ship the upgrade-flow system? Yes. The React hook, the decision matrix, the server-fn, and the paywall modal ship in TanStack Ship. The default variant is limit-hit for cap-shaped products and workflow-completion for B2C. The A/B harness ships under src/billing/ab.ts.

What is the highest-leverage variable in the upgrade flow? The timing of the prompt. The right prompt at the wrong time converts at <1%; the right prompt at the right moment (cap-hit or workflow-complete) converts at 8–12%. The decision matrix above is the production version of this rule.


Get started with TanStack Ship — the billing module ships the 5-variant upgrade-flow A/B framework, the decision matrix, and the paywall modal. See the pricing module → · See the features →