<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>RevenueCat Blog</title>
    <link>https://www.revenuecat.com/blog</link>
    <description>Insights about subscription apps, monetization and growth — by RevenueCat.</description>
    <language>en-US</language>
    <lastBuildDate>Mon, 28 Sep 2026 09:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.revenuecat.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title><![CDATA[How long should your free trial be? Data from 17,000+ apps]]></title>
      <link>https://www.revenuecat.com/blog/growth/free-trial-length</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/free-trial-length</guid>
      <pubDate>Mon, 28 Sep 2026 09:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Margarita Loktionova]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[We analyzed 17,000+ apps to see how free trial length relates to conversion and retention, from weekly subscriptions to annual plans.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/ec62fb8c8826094fefd5b017e9747e974c4403da-3200x1700.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Free trials keep dividing app developers. Give users more time, get paid faster, or skip the trial entirely?</p>
<p>Sure, shorter trials mean earlier payments, faster feedback for paid campaigns, and lower costs of providing free access. That last one matters most if you’re running an AI app, where every free session carries a real, variable cost behind it.</p>
<p>Yet our State of Subscription Apps 2026 report found a 42.5% median conversion rate for trials of 17+ days, versus 25.5% for trials of four days or less. That’s roughly 67% higher.</p>
<p>But do those subscribers keep paying? And does the pattern differ depending on subscription plans?</p>
<p>We analyzed 17,000+ apps to benchmark conversion and renewal across trial durations for weekly, monthly, and annual subscriptions. We also compared AI and non-AI apps and explored differences by category and subscriber region.</p>
<h2>Which trial lengths do high-volume apps choose?</h2>
<p>First things first: we looked at the 100 apps with the most trial starts in each category and plan duration, and checked what trial length each one runs.</p>
<p>Short trials tend to dominate. On weekly plans, 81–100 of those apps use trials of four days or less, depending on category.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b95c2ef046ff518c583822d025e68a5a2bc879b8-3842x2800.png" alt="Weekly subscription trial lengths among each category’s top 100 apps. Trials of four days or less dominate every category, ranging from 81% in Health &amp; Fitness to 100% in Gaming."/></figure>
<p>Monthly and annual plans also lean toward trials of nine days or less, although the mix varies by category.</p>
<p>Education, Health &amp; Fitness, and Travel favor 5–9-day trials, while Productivity and Photo &amp; Video favor trials of four days or less. One possible explanation is how long users need to evaluate the app: some benefits take repeated use to assess, while others are apparent in a single session.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/a8b07bcffee52aba8602b501cbe55dc661dcbccf-1921x2400.png" alt="Trial length distribution among each category’s top 100 apps for monthly and annual subscriptions. Trials of 5–9 days lead in most categories, while trials of four days or less are most common in Gaming and Photo &amp; Video."/></figure>
<p>These are apps with the most trial starts, rather than the highest revenue or retention. Their choices show what’s common at scale. But this doesn’t establish which trial length <em>performs </em>best.</p>
<p>That leaves a useful question: how do conversion and renewal compare across those common short trials? And what changes in the less common, longer-trial groups?</p>
<h2>How many trial users reach a second payment?</h2>
<p>Our data actually shows that longer trials are often associated with higher renewal rates, though the pattern varies by subscription plan:</p>
<ul>
<li><strong>Weekly subscriptions:</strong> The share of trial users who paid and renewed once is slightly higher after 5–9-day trials than after trials of four days or less: <strong>14.4% vs. 11.8%</strong>. Evidence for longer trials remains limited.</li>
<li><strong>Monthly subscriptions:</strong> Longer trials are associated with more trial users paying and renewing: <strong>30.6%</strong> after 10–16-day trials vs. <strong>18.5% </strong>after trials of four days or less. The pattern then levels off in the 17–32-day trial group.</li>
<li><strong>Annual subscriptions:</strong> The share of trial users who paid and renewed increases with trial length across all four groups, from <strong>3.5%</strong> after trials of four days or less to<strong> 18.5% </strong>after 17–32-day trials. Though trial duration alone may not explain higher retention.</li>
</ul>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/17cf835cbccd288d0ddc8c22c2199f75148050a8-1921x1080.png" alt="Share of trials that convert to paid subscriptions and renew once, by plan and trial length. Monthly plans rise from 18% to around 30%, and annual plans from 3% to 18%. Weekly estimates for trials longer than nine days are based on fewer than 20 apps."/></figure>
<p><em>One caveat before we go further: there is a correlational pattern, and a flattering one. Someone who’s still in your trial on day 25 of a 30-day window is simply more likely to pay than someone who bails on day two. Keep that in mind as the ceilings and floors get more specific below.</em></p>
<h2>Do conversion and renewal favor the same trial length?</h2>
<p>We also looked at conversion and first renewal separately to see whether longer trials coincide with more users paying, more paying subscribers renewing, or both.</p>
<p>For<strong> weekly</strong> subscriptions, the difference between the two common short-trial groups is larger at renewal. Trials of 5–9 days convert 24.3% of trial users, compared with 22.3% for trials of four days or less. Among paying subscribers, 65.9% renew after 5–9-day trials, versus 57.9% after shorter trials.</p>
<p>There are too few qualifying apps offering weekly plans with trials beyond nine days to extend that comparison reliably.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/cb69c0787ba3a034c60784232fe3117dda4980b4-1921x1080.png" alt="Weekly subscription trial conversion and first renewal rates by trial length. Conversion is 22% for trials of four days or less and 24% for 5–9 days; first renewal among paying subscribers is 58% and 66%, respectively."/></figure>
<p>For<strong> monthly</strong> subscriptions, conversion is higher in the 5–9-day trial group than in the shortest-trial group: 45.9% versus 39.6%. The 10–16-day group has similar conversion, at 46.6%, but higher first renewal: 72.0% versus 62.8% of paying subscribers.</p>
<p>The 17–32-day group has the highest first renewal rate, at 77.5%, but lower conversion than either middle group, at 43.7%.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b0aafb8914b89db29588a7d5e78cd7f054b1a227-1921x1080.png" alt="Monthly subscription trial conversion and first renewal rates by trial length. Conversion is 40%, 46%, 47%, and 44% for trials of four days or less, 5–9, 10–16, and 17–32 days, respectively."/></figure>
<p>For <strong>annual</strong> subscriptions, both rates are higher across longer-trial groups. Comparing trials of four days or less with 17–32-day trials, conversion rise from 24% to 44.6%, and first renewal from 18.3% to 47.5%.</p>
<p>Of course, a trial rarely builds a year of loyalty on its own: a longer trial mostly gives annual buyers more time to be sure before a bigger, harder-to-reverse commitment. So the people who convert after that longer look might already be the ones less likely to cancel.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/2f2620df57e66936da0b2d994fa4845d79618764-1921x1080.png" alt="Annual subscription trial conversion and first renewal rates by trial length. Conversion is 24%, 33%, 43%, and 45% for trials of four days or less, 5–9, 10–16, and 17–32 days, respectively."/></figure>
<h2>What if you remove the trial altogether?</h2>
<p>Here’s another interesting finding: subscriptions purchased without a trial have lower first-renewal rates on weekly and monthly plans than those that begin with one.</p>
<p>The weekly gap is particularly large: 35.9% of paying subscribers renew without a trial, compared with 57.9% after trials of four days or less and 65.9% after 5–9-day trials.</p>
<p>Annual plans show a different pattern. No-trial renewal is higher than renewal after trials of up to nine days, but lower than after longer trials.</p>
<table>
<thead><tr>
<th><p><strong>Trial length</strong></p></th>
<th><p><strong>Weekly (1st renewal)</strong></p></th>
<th><p><strong>Monthly (1st renewal)</strong></p></th>
<th><p><strong>Annual (1st renewal)</strong></p></th>
</tr></thead>
<tbody>
<tr>
<td><p>No trial</p></td>
<td><p>35.9%</p></td>
<td><p>49.5%</p></td>
<td><p>26.6%</p></td>
</tr>
<tr>
<td><p>Trial of four days or less</p></td>
<td><p>57.9%</p></td>
<td><p>54.2%</p></td>
<td><p>18.3%</p></td>
</tr>
<tr>
<td><p>Trial of 5–9 days</p></td>
<td><p>65.9%</p></td>
<td><p>62.8%</p></td>
<td><p>25.3%</p></td>
</tr>
<tr>
<td><p>Trial of 10–16 days</p></td>
<td><p>Too few apps</p></td>
<td><p>72%</p></td>
<td><p>36.4%</p></td>
</tr>
<tr>
<td><p>Trial of 17–32 days</p></td>
<td><p>Too few apps</p></td>
<td><p>77.5%</p></td>
<td><p>47.5%</p></td>
</tr>
</tbody>
</table>
<p><em>Note: this table measures first renewal among paying subscribers, not all trial users, so these numbers run higher. This comparison a;sp doesn’t tell us how many people purchase when shown a no-trial offer.</em></p>
<p>Lower renewal doesn’t automatically make a no-trial offer less profitable, though. <a href="https://www.revenuecat.com/blog/growth/should-your-app-stop-offering-free-trials">David Vargas’s case study</a> found that removing the trial, alongside changes to pricing and plans, helped one app earn enough per paying customer to support paid acquisition without it.</p>
<h2>How does trial length relate to conversion and renewal in AI apps?</h2>
<p>For AI apps with usage-based inference or generation costs, extra free access needs to justify its expense. Besides, <a href="https://www.revenuecat.com/blog/growth/ai-app-retention-study">higher churn</a> with lower conversions means there's less room to get the length wrong.</p>
<p>The <strong>monthly</strong> results offer a useful comparison.</p>
<p>Trials of 5–9 days and 10–16 days show almost identical conversion, at 38.2% and 38.5%. First renewal is higher in the 10–16-day group, at 64.2% versus 57.4%.</p>
<p>But the 17–32-day group shows no further renewal gain: first renewal is 64.1%, while conversion falls to 31.8%. In this dataset, the longest monthly trials don’t provide a stronger conversion or renewal benchmark than 10–16-day trials.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/2b8f108620e9e08eb0571774cafdb4dab29ddc3f-1921x1800.png" alt="For monthly subscriptions, AI apps have lower trial conversion and first renewal rates than non-AI apps at every trial length. Beyond 16 days, AI trial conversion falls from 39% to 32%, while first renewal stays at 64%."/></figure>
<p>For <strong>annual AI subscriptions</strong>, conversion is 33.8% after 10–16-day trials, compared with 23.3% after 5–9-day trials. First renewal is also higher, at 29.4% versus 19.6%. For <strong>weekly AI subscriptions</strong>, the difference is smaller. Conversion is 23.1% after 5–9-day trials versus 22.3% after trials of four days or less; first renewal is 62.5% versus 57.9%.</p>
<p>Without a trial, first renewal is lower on weekly and monthly AI plans. Annual no-trial renewal is 18.9%, compared with 15.1% after trials of four days or less and 19.6% after 5–9-day trials.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/27ccd923b1205b3e7ed58467fffa32a972b71240-3842x2160.png" alt="First renewal rates for AI subscriptions by trial duration and plan. Weekly rates rise from 33.8% without a trial to 62.5% with 5–9-day trials. Monthly rates rise from 41.3% without a trial to around 64% with 10–32-day trials. Annual rates range from 15.1% to 29.4%."/></figure>
<h2>Does giving users more trial time improve conversion across regions?</h2>
<p>On <strong>annual</strong> plans, North America and Western Europe have higher conversion across progressively longer-trial groups. Asia-Pacific follows that pattern up to 10–16 days, but has lower conversion in the 17–32-day group.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/285979b02bcfc31113442b6f62ecef3f718aa4c6-1921x1080.png" alt="Annual subscription trial conversion by trial length across seven regions. Conversion rises through 10–16 days in every region. At 17–32 days, it increases further in North America, Western Europe, and Latin America but declines elsewhere. North America has the highest rates; India &amp; Southeast Asia has the lowest."/></figure>
<p>On <strong>monthly plans</strong>, conversion in Middle East and Africa, India and Southeast Asia, and Latin America is highest in the 5–9-day group and lower in the 17–32-day group. The largest gap among these regions is in Middle East and Africa: 38.3% versus 27.4%.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/52a2bea226f311b9b509e7dbfa3567d774991874-1921x1080.png" alt="Monthly subscription trial conversion by trial length across seven regions. North America and Asia-Pacific have the highest rates, while India &amp; Southeast Asia has the lowest."/></figure>
<h2>Do the same trial lengths outperform across app categories?</h2>
<p>On monthly plans, longer-trial groups generally have higher renewal rates across app categories.</p>
<p>But they don’t always have higher conversion. In many categories, conversion is highest in the 10–16-day group and lower in the 17–32-day group.</p>
<p>When interpreting these differences, consider two timelines: how quickly users recognize the app’s value, and how often they have a reason to return. The data don’t measure habit formation, but those questions can help you decide what extra trial days should accomplish.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b726329a5c624396c25e04b83cefb11bb604d829-1921x1500.png" alt="Heatmap of monthly subscription trial conversion and first renewal rates across 11 app categories and four trial lengths. Conversion patterns vary by category, while longer trials generally have higher first renewal rates among paying subscribers. Dashed borders mark results based on fewer than 20 apps."/></figure>
<p>Three categories illustrate the differences:</p>
<ul>
<li><strong>Media &amp; Entertainment</strong>: The 10–16-day group leads on both conversion (58.9%) and first renewal (79.4%), with neither rate higher in the 17–32-day group. For streaming apps, the question to explore is whether extra trial days help users discover more content they want to keep watching.</li>
<li><strong>Utilities</strong>: Conversion is highest in the 10–16-day group, at 47.4%, compared with 44.0% in the 17–32-day group. First renewal is highest in the longest group, at 77.9%, versus 55% for trials of four days or less. A user might recognize a tool’s value after one task. The question for a longer trial is whether it gives them opportunities to use it again.</li>
<li><strong>Health &amp; Fitness</strong>: Conversion is higher in the 5–9-day group than in the 10–16-day group: 46.8% versus 40.4%. First renewal is higher across longer-trial groups, from 51.5% for trials of four days or less to 77.1% for 17–32-day trials. If you’re considering more trial time, check whether users keep completing workouts or logging progress during those extra days. That would give you evidence of continued engagement, rather than assuming a longer trial builds a habit.</li>
</ul>
<h2>So, what's the right trial length for your app?</h2>
<p>These findings make a case for challenging a short trial chosen by default. But they also show where longer trials have no clear advantage, helping you narrow down which alternatives deserve a test.</p>
<p>At the end of the day, the answer isn't &quot;run longer (or shorter) trials,&quot; &quot;cut trials entirely,&quot; or a number copied from the leader in your niche. It's whichever pattern matches your app:</p>
<ul>
<li><strong>Time to value</strong>. How long a user needs to experience your app before they can fairly judge whether it's worth paying for.</li>
<li><strong>Habit versus motivation</strong>. Utilities' renewal kept climbing long after conversion had already peaked, because deciding fast and building a routine aren't the same thing. Health &amp; Fitness runs the same tension the other way: a longer trial builds the habit, but it also gives motivation more time to fade first.</li>
<li><strong>Audience context</strong>. Price sensitivity and trust in recurring billing can change what a longer trial buys: more confidence to pay, or just more time to back out. So does whether users are weighing you against a free alternative, and how much they need to learn before they can fairly judge your app at all.</li>
<li><strong>Cost</strong>. Longer trial delays revenue and stretches out how long paid acquisition takes to pay for itself. If your app has a real per-user cost behind every session, that spend adds up whether or not the person converts.</li>
</ul>
<p>The best next action? Use the benchmarks above as a starting point, then test trial length against your own subscription plans, your app's specifics, and what you know about your audience.</p>
<p><a href="https://www.revenuecat.com/feature/experiments">RevenueCat Experiments</a> lets you get there faster. Set trial length as the variable, and let your own revenue numbers make the call.</p>
<h2>How we analyzed the data</h2>
<p>We compared trial conversion and paid renewal separately for weekly, monthly, and annual subscriptions on the App Store and Google Play, with breakdowns by platform, AI classification, category, and subscriber region. We also looked at the 100 apps with the most trial starts in each category.</p>
<ul>
<li><strong>Period:</strong> August 2025–July 2026, based on trial starts for conversion and renewal dates for retention. The combined trial-to-first-renewal measure follows the same trials through both payments, including earlier annual cohorts.</li>
<li><strong>Measurement:</strong> Median app-level rates. Each renewal rate measures subscriptions that paid again, conditional on the previous payment.</li>
<li><strong>Sample requirements:</strong> More than 100 eligible trials or subscriptions reaching first renewal per qualifying app segment. Groups with fewer than 20 apps are directional.</li>
<li><strong>Trial coverage:</strong> No-trial subscriptions appear only in renewal comparisons. Trials longer than 32 days are excluded from conversion figures.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[César Pinto Castillo's $150 Mac could not build iPhone apps]]></title>
      <link>https://www.revenuecat.com/blog/growth/cesar-pinto-castillo-sofia-larsson-ambre-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/cesar-pinto-castillo-sofia-larsson-ambre-launched-podcast-2026</guid>
      <pubDate>Wed, 23 Sep 2026 13:08:51 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[César Pinto Castillo built Ambre to test CloudKit and forgot about it; Sofia Larsson, a textile engineer, rewrote it to learn Swift — and it ended up featured on the App Store.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/3299eec284b5d3fca8b397ecd42d2753452269c9-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Ambre is a recipe app built by two people in Gothenburg, Sweden. César Pinto Castillo wrote the first version to test a new Apple framework, put it on the App Store, and forgot about it. Sofia Larsson, who has a bachelor's in textile engineering and no formal software training, rewrote its views to learn SwiftUI and never stopped. On this episode of Launched, Charlie Chapman gets both origin stories, the Apple program that turned a beginner into a co-founder, and a closing argument about what's left to compete on when AI can build the features.</p>
<p><a href="https://www.youtube.com/watch?v=y2xmqABa3EM">Watch on YouTube</a></p>
<h2><strong>The $150 Mac that couldn't build iOS apps</strong></h2>
<p>César had a computer science degree and a job writing C# for an ad agency when he walked past a shop window in Västerås and saw an iPhone in demo mode, scrolling through contacts. He'd been building for a Sony Ericsson Xperia X1 and hated everything about it. &quot;I just saw it and I was like, 'Oh my God, this is amazing.'&quot;</p>
<p>That night, a well-known Swedish comedian posted asking whether someone could make him an app. &quot;And I have no idea how to build an app. I didn't have a Mac. I didn't know what Xcode was. I knew nothing,&quot; César says. Charlie's follow-up: &quot;Well, you didn't even have an iPhone at this point, right? You just saw one?&quot; He hadn't. He wrote back anyway, agreed a price, and went back to the store to ask what he'd need.</p>
<p>They sold him a returned PowerBook G4 for $150. He installed Xcode, opened the sample project, hit build, and discovered that iOS apps needed an Intel Mac to compile. &quot;So I had to spend all the money I was getting paid to build the app to buy my first Mac. So that's how I got into app development.&quot;</p>
<p>The app itself was a blog reader: a WordPress plugin he wrote to expose posts as JSON, pulled into a list. &quot;When there only was one screen size,&quot; as he puts it. The commitment came before the tooling, and the fee paid for the tooling. He's been employed as an iOS engineer since 2011.</p>
<h2><strong>A CloudKit experiment, forgotten, then handed to a beginner</strong></h2>
<p>Around the time CloudKit launched, César built a recipe app to see what the framework could do. Recipes were CloudKit records; you could see other people's. He named it Cooking, shipped it, and moved on. &quot;I put it out there and I forgot about it.&quot;</p>
<p>It kept being used. One user had more than a hundred recipes in it, which mattered because of a detail Sofia raises: you couldn't reorder instructions or ingredients. Get a step wrong and you redid the recipe. Charlie: &quot;This is the classic looking for some app as an excuse to try out a piece of technology.&quot;</p>
<p>Years later, Sofia wanted to learn to code. She'd started with Swift Playgrounds on an iPad, steering a character called Byte through a maze. César had an app lying around that he wanted rebuilt in SwiftUI. &quot;Why don't you just rewrite all the views?&quot; Charlie's verdict: &quot;Easy first task.&quot;</p>
<p>&quot;I started just building up the views and I guess I never stopped because I'm still building the views.&quot; Her biggest obstacle wasn't the logic. &quot;I didn't have any terminology. So when I wanted to Google for something, I had to describe what I wanted.&quot; Even the word <em>modifier</em> wasn't hers yet. Cooking became Ambre, and Ambre started getting featured on the App Store.</p>
<h2><strong>Apple said the call was of utmost importance. It was a two-hour technical interview.</strong></h2>
<p>Sofia's plan was to get good enough to be hired as a developer somewhere. Then César saw a post from Apple's Mike Stern about Apple Entrepreneur Camp and told her to apply.</p>
<p>&quot;I was terrified and César said, 'Oh, well, what's the worst thing that's going to happen?' They say no or they say yes. And I said, 'Yeah, that's the worst thing. They can say yes.'&quot; Because then she'd have to do it.</p>
<p>The email that came back said it was imperative to get a call scheduled that week. She asked César what Apple could want. His guess: a five-minute welcome call, they just want to tell you you're in. &quot;Oh, it turned out it was a technical interview for two hours.&quot;</p>
<p>She got in. The camp ran virtually during the pandemic, two weeks of DTS hours with Apple engineers, design sessions with Apple designers, and pitch training, on a nine-hour time difference. &quot;We literally slept and ate and coded.&quot; What she took from it, she says, is going to sound cheesy: &quot;my confidence, because going in, I felt like such an imposter. I was nervous at every meeting thinking, oh, they're going to call me out.&quot; The environment did the opposite. Her first client project followed, with César, across three time zones.</p>
<h2><strong>Everyone posted code. She posted about color.</strong></h2>
<p>Ambre's marketing, by their own account, has been light: an Instagram account that mostly works as a way for users to reach them. What grew was Sofia's own presence, after César and a contact at Apple all but pushed her into it.</p>
<p>She posted about app development with a tilt toward UI and design, and it found an audience. Her explanation: the community's feed was mostly code — someone found a new API or SDK and tried it out. &quot;Whereas I would like, ooh, this color combination is super nice. And if you put it this way in the layout, it would read much better.&quot; Being one of the few people talking about design in a stream of code posts was the differentiation.</p>
<p>Charlie's own first memory of them is a marketing move too. On the roof of the Apple Park Visitor Center, Sofia handed him a card with a QR code. It opened Ambre as an App Clip, inside which another QR code led to a recipe: how to make Swedish fish. Then she pulled a bag of actual Swedish fish out and handed it over. &quot;I watched you do this to other people after me, and we all had the same reaction.&quot; His conclusion at the time: these are very fun people.</p>
<h2><strong>Does the code really matter anymore?</strong></h2>
<p>The episode ends where the edit opens, on a question that a designer-and-engineer pair are unusually well placed to argue. César's position: &quot;With AI today, we can build so much stuff. The thing that's going to put you apart or your product apart from the competition is the experience you offer.&quot; The feature set won't do it; design is about to matter more, not less.</p>
<p>He borrows an analogy from a Linux figure he'd seen quoted: complaining that you don't know what AI-generated code is doing is like complaining that you don't read the assembly. &quot;What we own is the UX and quality of our app. Our customers are not going to read the code.&quot;</p>
<p>Sofia's version comes from the toolbox. &quot;Procreate is a tool. An iPad is a tool. A pencil is a tool. When you buy watercolor, it's pigment and something that bind the pigment.&quot; AI is the next one, and it opens the door for designers who had the idea in their head but not the means to build it. This year that's been her: &quot;I can talk directly with Claude and see my ideas come to life within minutes, hours, depending on the complexity.&quot;</p>
<p>Charlie pushes back a little — AI-built things feel similar unless someone is making all the small choices — and César closes with the question he wants developers to sit with: &quot;If you make sure QA is awesome, the app looks amazing and it solves a real problem, does the code details really matter?&quot;</p>
<p>Also<a href="https://www.youtube.com/watch?v=y2xmqABa3EM" target="_blank" rel="noopener noreferrer"> in the full episode</a>: what a textile engineer actually studies (geotextiles under airport runways, a thesis on getting a medical bandage to a CE standard), César's detours through blackjack dealing and Volvo factory software, the designers Sofia keeps a running iMessage thread with herself about, and the Ambre update landing with iOS 27.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://www.linkedin.com/in/sofiachristinalarsson/">Sofia Larsson on LinkedIn</a></li>
<li><a href="https://www.linkedin.com/in/jagcesar/">César Pinto Castillo on LinkedIn</a></li>
<li><a href="https://apps.apple.com/us/app/ambre-recipe-book-organizer/id1102236212">Ambre on the App Store</a></li>
<li><a href="https://ambi.se/">Ambi Studio</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[App Growth Annual 2026: full agenda and speaker lineup ]]></title>
      <link>https://www.revenuecat.com/blog/company/app-growth-annual-agenda-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/app-growth-annual-agenda-2026</guid>
      <pubDate>Wed, 23 Sep 2026 10:20:23 GMT</pubDate>
      <dc:creator><![CDATA[Lorelei Whitman]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[Five keynotes, 36 workshops, and one day worth clearing your meetings for]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/310d6e75ce2e5735af615a421607a4e53d2780d4-1600x850.gif" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>We’re less than a month away from <strong>App Growth Annual</strong>, RevenueCat’s event for the people building, growing, and shaping what’s next in apps. On <strong>October 21</strong>, thousands of app professionals will be gathering in New York and online worldwide for a day packed with candid lessons from some of the biggest names in the App Store.</p>
<p>We’ve been polishing the agenda for months, and it’s finally ready for your eyes: five keynotes and 36 workshops, split across exclusive virtual and in-person content. So whether you’re joining us in NYC or from the comfort of your home office, you’ll be leaving with fresh ideas and the momentum to ship them.</p>
<aside class="tip"><strong>App Growth Annual is just one part of a bigger week</strong><p>The app economy is taking over New York from October 19 for <a href="https://www.appweek.events/" target="_blank" rel="noopener noreferrer">New York App Week</a>: four days of community-led events across the city, culminating in The Shippies on October 20 and App Growth Annual on October 21.</p></aside>
<h2><strong>Keynotes</strong></h2>
<p>Here’s what’s heading up the main stage: five sessions from app leaders about what worked, what didn’t, and what they’d do differently — straight from the people building the apps.</p>
<p>All the keynotes will be available to watch on YouTube after the event. So even if October 21 is back-to-back meetings for you, there’s no need to miss anything. <a href="https://appgrowthannual.com/virtual">Register now</a> to get sent the replays as soon as they drop.</p>
<h3><strong>Confidence engineering: why your app's activation flow is probably too short — Ravi Mehta, ex-CPO, Tinder</strong></h3>
<p>Every mobile growth team obsesses over friction — fewer screens, fewer fields, fewer taps between install and activation. It's conventional wisdom, and it's incomplete. One healthcare app replaced a three-step checkout with a twenty-five-step intake flow and conversion jumped 40%.</p>
<p>At Tinder, Ravi's team did the opposite — stripping onboarding to under two minutes to pull in millions who'd never have finished an eHarmony profile. In this session he'll share a framework for diagnosing your own flow: when to strip friction to the bone, when to add steps that earn trust, and how to spot the 'death zone' where teams split the difference and get the worst of both.</p>
<p><strong>You'll leave with</strong>: a way to audit your own funnel, step-by-step, and a sharper question than &quot;should this be shorter?&quot;</p>
<h3><strong>Breaking the rules on purpose: the contrarian bets that built Babbel — Julie Hansen, CRO &amp; US CEO, Babbel</strong></h3>
<p>Every subscription app has heard the same advice: never sell lifetime deals, don't bother with web checkout, go freemium to keep the funnel full, spend only on acquisition… Well, Babbel has ignored most of the advice — and is still here, 30 million subscriptions later.</p>
<p>As Babbel’s US CEO for nearly a decade, Julie had a hand in most of those bets. She’ll walk through why they made them against the advice of the day, which ones they’d make again, and what they cost Babbel.</p>
<p><strong>You’ll leave with: </strong>the insight to discern which rules in your own playbook are real, and which are just habits the industry hasn’t questioned yet.</p>
<h3><strong>Built to burn: how to turn a high-churn product into good subscription economics — Greg Cohn, Co-founder &amp; CEO, Burner (Ad Hoc Labs)</strong></h3>
<p>Burner is an app for disposable phone numbers, with a button that destroys the very thing you're paying for… yet it has been growing and profitable for almost a decade. In fact, half of Burner’s new subscription revenue on iOS is from users who previously churned.</p>
<p>Greg says they didn’t plan it that way. But along the ride, they learned not all churn is failure — some of it, in fact, is success. His session will tell the story of that success: from designing for profitable churn, to cohort math and monetization architecture.</p>
<p><strong>You’ll leave with: </strong>analytical frameworks and mechanics you can steal — and a sneak preview of the new-to-the-market retention hack Burner’s testing next.</p>
<h3><strong>Speedrunning subscription monetization: what Tolan learned when the AI bill came due — Ajay Mehta, Founder, Tolan</strong></h3>
<p>For a decade, subscription apps ran on one assumption: the marginal cost of a subscriber is roughly zero. AI broke that. Now free users cost real money, and the heaviest users can cost more than they pay.</p>
<p>At Tolan, they learned this lesson in week one, when the OpenAI bill outgrew their userbase and a paywall became a survival mechanism. Since then, the team has run a decade’s worth of monetization plays in double-time. Ajay will recount each of these decisions; what they expected, what actually happened, and how they came to their ultimate monetization rule: would they be happy explaining it on a podcast?</p>
<p><strong>You’ll leave with:</strong> a sense of what the end of zero marginal cost means for your own pricing, whether you're building an AI app today or adding AI to the one you have.</p>
<h3><strong>RevenueCat product update ahead of 2027 — Jacob Eiting, CEO &amp; Co-founder, and Miguel Carranza, CTO &amp; Co-founder, RevenueCat</strong></h3>
<p>We won’t spoil too much about our product keynote, but you can expect a retro on the last 12 months, roadmap reveals, and live product launches. </p>
<p><strong>You’ll leave with: </strong>an insider's look at what's next for RevenueCat.</p>
<h2><strong>In-person workshops (block one)</strong></h2>
<p>One of App Growth Annual’s biggest opportunities is getting the right people in one room, asking the same questions you’ve been wondering about. Whatever your focus is right now, there’s a hands-on session to help, filled with genuinely useful conversations.</p>
<h3><strong>Apple Ads 2026, beyond the basics: what changed, what matters, and how to act on it — Thomas Petit, Growth Advisor</strong></h3>
<p>Apple Ads has changed more in the last two years than in the eight before it. This interactive session skips the 101 and tackles four core questions: Which numbers can you trust? Should you let Apple decide how you buy inventory? How do you test and iterate on creatives? And how much should you really buy?</p>
<p>Each question is unpacked with Thomas’s personal experience, how to use the functionality, and where the numbers can fool you. Expect questions, strong takes, and polite disagreement. Not recommended for ASA beginners.</p>
<p><strong>You’ll leave with: </strong>a varied set of perspectives and focused examples to help inform complex ASA decisions.</p>
<h3><strong>Signal engineering: build your subscription app signal plan — Shumel Lais, Co-founder, Day30</strong></h3>
<p>Want to send better signals to Meta, Google, and TikTok, but your event tracking and measurement setup isn’t ready for it?</p>
<p>Shumel’s workshop will have attendees audit their own subscription funnel from install through renewal, scoring your existing events against speed, volume, intent, commercial value, and measurement reliability. Take part to identify which signals to test first and what you need to fix before you begin.</p>
<p><strong>You’ll leave with: </strong>a signal readiness scorecard and prioritized action plan for your growth and data teams.</p>
<h3><strong>The executive roundtable: networking and problem-solving with fellow leaders at scaled apps — Ron Schneidermann, CEO, Acely</strong></h3>
<p>No agenda, no slides — just candid discussion and problem-solving with a small group of fellow app executives. Ron kicks the session off with lessons from scaling AllTrails to millions of subscribers, then opens the floor to questions and discussions on team building, product decisions, and growth challenges.</p>
<p><strong>You’ll leave with: </strong>fresh perspectives from peers who’ve been there too.</p>
<h3><strong>The viral iteration loop: adapt trending content into high-converting creative — Diego Kafie, CEO &amp; Co-founder, Noise</strong></h3>
<p>Viral content already exists in your niche — the opportunity is turning it into creative that drives installs for your app. In this workshop attendees will search live for what's working on TikTok and Instagram in your category, break down why it's performing, and adapt it into iterations you can test as paid or organic creative.</p>
<p><strong>You’ll leave with: </strong>a shortlist of proven content angles tailored to your app, and a repeatable process for finding and adapting them.</p>
<h3><strong>How AI is changing subscription app monetization — Phil Carter, Founder and CEO, Elemental Growth</strong></h3>
<p>LLM-powered features carry non-trivial costs, but most subscription apps are still approaching monetization as though their gross margins haven't changed. Spend this session reviewing the strategies that the best AI apps are using to adapt: restricting freemium, shortening trials, adding higher-priced AI tiers. Wrap up by benchmarking your own metrics against peer apps and apply proven pricing tools (Van Westendorp, Gabor-Granger, MaxDiff, Conjoint) to your own packaging.</p>
<p><strong>You’ll leave with: </strong>a benchmarking tool, a pricing study template, and playbooks for adapting your monetization to the AI era.</p>
<h3><strong>How to launch and grow your app in Japan and Korea — Hyerin Choe, Customer Success Lead, AB180</strong></h3>
<p>Japan and Korea are two of the highest-spending mobile markets in the world, but most Western apps stall at entry because the playbook that works in the US doesn't translate. Join Hyerin Choe from Korean app growth agency, AB180, to identify where your current growth strategy breaks down and build a localized go-to-market plan.</p>
<p><strong>You’ll leave with: </strong>a market-entry framework covering channels, positioning, and the cultural signals that data alone won't reveal.</p>
<h3><strong>Build a user research system that runs itself — Ekaterina Gamsriegler, Growth Consultant &amp; Director of Growth at DataCamp</strong></h3>
<p>Most apps run user research in bursts, then go dark for months. Ekaterina will walk through how to design an always-on feedback system for your app instead. From choosing the right research inputs (cancellation surveys, in-app prompts, price sensitivity polls), to writing the templates, mapping distribution channels, and building a cadence that feeds insights into your experimentation pipeline without becoming a full-time job.</p>
<p><strong>You’ll leave with: </strong>a research system that’s ready to launch next week.</p>
<h3><strong>Beyond the first growth hire: building a remote growth and marketing team that scales from 0 to $50M — Olivier Lemarié, VP of Growth, Photoroom</strong></h3>
<p>Growth can't stay founder-led forever, but knowing when to specialize, who should own what, and which capability to build next is where scaling companies stall. Olivier’s workshop helps you map your current growth org against the capabilities needed to scale subscription revenue — paid, lifecycle, product growth, pricing, creative, data, international — and identify ownership gaps, missing roles, and your next hires.</p>
<p><strong>You’ll leave with: </strong>a growth org map and prioritized hiring plan for your next stage.</p>
<h3><strong>Roast my ASO: your App Store listing, audited live — Steve P. Young, Founder, App Masters</strong></h3>
<p>Bring your App Store listing and get it torn apart live. Steve will audit real participants' metadata, screenshots, and keyword strategies against what's actually driving installs in their category — with pre-prepared case studies showing where AI-assisted ASO is outperforming manual approaches.</p>
<p><strong>You’ll leave with: </strong>knowledge of exactly what’s costing you rankings and downloads — and what to fix first.</p>
<h3><strong>Find the moment your app earns the right to ask for money — Olha Yohansen, Independent Growth Consultant</strong></h3>
<p>Has your app earned the user’s trust by the time it asks for money? Olha’s workshop will get you mapping your own onboarding to pinpoint where users first feel “this app gets me” — and check whether that moment lands before or after you ask them to pay. If it's after, you're converting on template momentum, not trust.</p>
<p><strong>You’ll leave with: </strong>a redesigned onboarding sequence that puts the <em>aha! </em>moment where it belongs.</p>
<h2><strong>In-person workshops (block two)</strong></h2>
<p>Our second round of workshops offer 10 hands-on sessions packed with in-the-trenches tactics to grow your app. We know it’s hard to choose, but whichever session you attend, you’re guaranteed to leave with new ideas and experiments to try right away.</p>
<h3><strong>Meta UA in 2026: what still works, what's changed, and how scale changes the game — Gessica Bicego, Fractional CMO, Independent Consultant</strong></h3>
<p>The right Meta campaign structure depends on your spend level, but most teams use the same setup whether they're spending $10K or $500K a month. Gessica’s workshop will group attendees by budget range where you’ll work through how campaign structure, optimisation approach, setup (CBO, Advantage+, W2A, signal engineering), and creative strategy should adapt for your scale.</p>
<p><strong>You’ll leave with: </strong>understanding of which structure fits your current spend and what to change as you grow.</p>
<h3><strong>Google Ads for iOS: how to make it work — Ashley Black, Founder &amp; CEO, Candid Consulting</strong></h3>
<p>Google has been overlooked as a serious iOS acquisition channel, but the platform has changed, and the opportunity is bigger than most teams realize. Join Ashley to evaluate your own app against Google's current campaign types, identify which formats match your funnel and audience, and stress-test whether the setup pain is worth the payoff for your specific scale and category.</p>
<p><strong>You’ll leave with: </strong>a clear yes/no on Google Ads for your app, and a plan for what to test first.</p>
<h3><strong>How your ad creative shapes your LTV — Nataliia Drozd, Independent Growth Expert</strong></h3>
<p>The creative that lowers your CPA isn't always the creative that attracts subscribers who stay. Viral hooks and trending formats can flood your funnel with users whose expectations don't match your product — and you won't see the damage until retention data catches up. In Nataliia’s workshop, you’ll map the relationship between your creative narratives and the customer segments they attract, then test your assumptions in a simulation using real campaign data.</p>
<p><strong>You’ll leave with: </strong>knowledge of which creative angles to scale, and which are quietly eroding your LTV…</p>
<h3><strong>Hybrid monetization: add new revenue without hurting your subscription — Cristian Rotari, Independent Consultant</strong></h3>
<p>Not every user will subscribe, but that doesn't mean they're worth zero. In this session you’ll map your user segments, identify which audiences are under-monetized, and evaluate which hybrid tactics fit your product: ads, IAPs, credits, affiliates, partnerships.</p>
<p>Finish by building a monetization ladder for your app and running a cannibalization check to confirm new revenue streams are truly incremental, not cannibalizing existing subscriptions.</p>
<p><strong>You’ll leave with: </strong>one experiment that’s ready to test in the next 30 days.</p>
<h3><strong>Design habit mechanics that don't fade after week one — Yves Benchimol, CEO &amp; Co-founder, WeWard</strong></h3>
<p>Gamification mechanics lose their impact the moment novelty wears off. Most teams respond by layering on more features, rather than questioning whether the mechanic should exist at all. Join Yves to apply a motivation-first framework to your own product and identify what's actually driving repeated use. You’ll then evaluate which mechanics are earning their place, and find where simplifying or removing features would improve retention more than adding new ones.</p>
<p><strong>You’ll leave with: </strong>a clear map of which habit mechanics to keep, kill, or redesign.</p>
<h3><strong>Ship faster with AI without learning slower — Ethan Garr, Growth Advisor, Breakout Growth Labs</strong></h3>
<p>AI has collapsed the time between idea and shipped feature, but most teams haven't upgraded their learning loops to match. You're deploying faster, but are you discovering what users value faster, or just building confidently in the wrong direction?</p>
<p>Ethan will walk through how to map your current growth initiatives across a Velocity vs. Learning Matrix, to identify where AI is genuinely accelerating progress, and where speed is masking the absence of real customer signals.</p>
<p><strong>You’ll leave with: </strong>a prioritized view of where to let AI run and where human judgment is key to helping more users reach experiences they can’t live without.</p>
<h3><strong>Diagnose your biggest revenue bottleneck — Alice Muir Kocourková, Independent Growth and Subscription Consultant</strong></h3>
<p>You've spent the last quarter optimizing something, but was it the right thing? Most teams pour effort into onboarding when the real opportunity is renewal, or obsess over trial conversion when Month 3 churn is quietly draining more revenue.</p>
<p>Join Alice to score each stage of your subscription lifecycle against industry benchmarks, identify where the gap between your app and the norm is largest, and prioritize fixes using an Impact vs. Effort framework.</p>
<p><strong>You’ll leave with: </strong>your bottleneck diagnosed, and a 90-day plan to close it completely.</p>
<h3><strong>Segmentation as a growth engine: how to scale across segments without breaking efficiency — Bohdan Lopatiy, Product Director - Growth, Flo</strong></h3>
<p>Most teams know they should segment, then hit the cost of it: split your users enough ways and every test is underpowered, every cohort too small to read, and the efficiency you were chasing disappears into a hundred experiments nobody can act on.</p>
<p>For this workshop, you'll slice your user base several ways, work out which splits actually change what you'd build differently, and find the segment you're currently treating as an average when it isn't.</p>
<p><strong>You’ll leave with: </strong>a segmentation model sized to your actual traffic.</p>
<h3><strong>From investment to exit: tactics for raising capital and selling your business — Eric Crowley, Managing Director, Houlihan Lokey</strong></h3>
<p>Whether you're preparing for a fundraise or positioning for acquisition, the gap between ‘growing nicely’ and ‘investor-ready’ is usually a handful of specific metrics and positioning decisions. Eric’s workshop will offer a chance to benchmark your business against what buyers and investors are <em>actually</em> looking for right now, identify gaps in your financial story, and pressure-test your positioning with the room.</p>
<p><strong>You’ll leave with: </strong>readiness to navigate your next round or exit conversation with confidence.</p>
<h3><strong>Roast my web funnel — Nathan Hudson, Founder, Perceptycs / Swoopy</strong></h3>
<p>Your web funnel follows the playbook. But the playbook doesn't account for your category, your traffic, or the assumptions baked into every ‘best practice’ you copied.</p>
<p>Come along to Nathan’s web funnel roast and put your funnel on screen to get it torn apart from ad to checkout: what's cargo-culted, what's actually earning its place, and which experiments carry hidden risk.</p>
<p><strong>You’ll leave with:</strong> prioritized test hypotheses and the context for why the obvious fix might not be yours.</p>
<h2><strong>In-person workshops (block three)</strong></h2>
<p>Wrapping up the day, our final block of workshops are definitely last but <em>not </em>least. Expect intimate conversations, practical takeaways, and new playbooks to take home.</p>
<h3><strong>Affiliate marketing: when it works and how to start — David Vargas, App Growth Consultant</strong></h3>
<p>Most apps treat affiliate marketing as an afterthought or assume it only works for e-commerce. But at scale, it can deliver high-intent users at a fraction of your Meta CPAs — if you pick the right partners and structure the right payouts.</p>
<p>David’s workshop will give you the chance to assess whether your app has the economics and category fit for affiliate marketing, map your ideal partner types, and design a commission structure that protects your margins.</p>
<p><strong>You’ll leave with: </strong>a go/no-go decision and a 30-day launch plan.</p>
<h3><strong>Build an organic-to-paid acquisition engine — Shane Troy, Co-founder &amp; Partner, Cousin Labs</strong></h3>
<p>Organic and paid teams rarely drive the same outcome, and the translation layer between them is where most growth teams leave the biggest gains on the table. In this session, you’ll map the exact loop that turns organic content into scalable paid creative: hypothesis design, creator selection and coaching, asset tagging, feedback loops, and the velocity payoff when winning formats feed directly into paid briefing.</p>
<p><strong>You’ll leave with: </strong>a playbook to connect your organic and paid programs immediately.</p>
<h3><strong>Your creative pipeline is broken and more AI won't fix it — Elise Zareie, Head of Paid Social, Lingokids</strong></h3>
<p>AI handles analysis, iteration, and automation well, but when it's producing your actual ad creative, you often get volume without differentiation, or content that erodes trust with your audience. Join Elise’s session to audit your current pipeline and separate where AI earns its place, versus where it’s producing sameness. Finish by designing the right mix for your vertical: what stays AI, what needs human creators, and how to build that alternative without blowing your budget.</p>
<p><strong>You’ll leave with:</strong> a creative production map tailored to your app and audience.</p>
<h3><strong>Ad monetization playbook for subscription apps — Felix Braberg, Ad Monetization, 2.5 Gamers</strong></h3>
<p>Your app is sitting on a monetization layer it's probably not using (or not using well). Ad revenue data reveals what each user segment is actually worth and whether they should be subscribing or staying on a free, ad-supported tier. This workshop covers how to read impression data as a willingness-to-pay signal, segment users accordingly, and set differentiated pricing.</p>
<p><strong>You’ll leave with: </strong>a segmentation framework and a step-by-step tracking setup guide.</p>
<h3><strong>Make your app a habit: the lifecycle messaging that keeps people coming back — George Deglin, Founder &amp; CEO, OneSignal</strong></h3>
<p>How do you make your app part of your customers' routine? Join this workshop to identify a behavior worth repeating, map where users drop off before they get there, and work out which of those moments can actually be fixed with messaging — and which are product problems a message won't solve. Wrap up by drafting a three-touchpoint journey with the audience, timing and rules behind it.</p>
<p><strong>You’ll leave with: </strong>a brief you can hand to your team, from draft messages to behavioral rules and a measurement plan.</p>
<h3><strong>From 1 to 100 A/B tests per month: a practical framework for scaling experimentation — Olga Berezovsky, Head of Data &amp; Analytics, Data Analysis Journal</strong></h3>
<p>Running more A/B tests doesn’t necessarily mean learning faster. As experimentation programs grow, tests often compete for the same traffic, run for months, and produce results that teams don’t trust. In this workshop, you’ll learn how to decide which ideas deserve a fully powered A/B test, which should be evaluated using faster methods, and which aren’t worth testing. You’ll then spend time scoring your current test portfolio by potential business value, identifying where rigor is slowing down learning, and redesigning your pipeline to complete more meaningful experiments.</p>
<p><strong>You’ll leave with: </strong>a prioritized experimentation roadmap sized to your actual traffic and resources.</p>
<h3><strong>Your AI marketing team: what it can run, what it can't, and what to do about the rest — Jessica Gotti, Head of Performance Marketing, Bend</strong></h3>
<p>One marketer with AI can now do the work of a small team. This session breaks marketing into single functions and asks one question for each: can AI run it alone, does it just help, or is it still a person's job? Each answer comes from practice — real projects where AI took over, helped, or failed.</p>
<p><strong>You’ll leave with: </strong>a map of the AI marketing team and real examples to make it work.</p>
<h3><strong>$1M to $10M ARR: the founder-to-CEO mistakes that stall your next stage — Josh Peleg, Vice President M&amp;A and Business Development, BlueThrone</strong></h3>
<p>The skills that got you to $1M ARR won't get you to $10M, but they’ll get you to this workshop and that’s pretty close. Join M&amp;A guru Josh to audit your own setup and mindset across four dimensions — growth, product, monetization, and team – and identify which founder habits are now bottlenecks. Bring your real growing pains; the room will workshop them together.</p>
<p><strong>You’ll leave with: </strong>knowledge of the three changes needed to unlock your next stage.</p>
<h3><strong>Fix your activation before it kills your retention — Daphne Tideman, Growth Advisor &amp; Consultant, Growth Waves</strong></h3>
<p>Your activation metric is probably measuring activity, not predicting revenue — which means every onboarding test you've shipped this year optimised for the wrong thing. Daphne’s session will help diagnose whether your drop-off is a trust, context, or value problem, before you’ll define the one user action that actually predicts LTV, and pressure-test it with the room.</p>
<p><strong>You’ll leave with: </strong>a validated activation hypothesis and one test to ship next week.</p>
<h3><strong>Hyper-personalization in app onboarding and monetization — Thomas Cotter, Principal, Elemental Growth</strong></h3>
<p>Many apps invest heavily in personalizing onboarding and monetization without seeing meaningful ROI. In this workshop, you’ll learn how to make personalization actually work: what information to collect, how to identify and prioritize opportunities, where and how to experiment, and what to measure. Thomas will walk through real examples, then put attendee apps on screen for live personalization teardowns and test ideas.</p>
<p><strong>You’ll leave with: </strong>a shortlist of personalization tests to run, prioritized by likely impact.</p>
<h2><strong>Virtual workshops</strong></h2>
<p>You didn’t think we’d neglect those watching from home? Our online attendees have six insightful sessions to choose from, but even better — unlike the in-person workshops, these will be recorded and shared to watch back after the day. So no need to panic if you can’t decide which one to tune in to!</p>
<h3><strong>Make AI-generated ads that actually convert — Julien Daver, Head of User Acquisition at SplitMetrics Growth Services</strong></h3>
<p>Generic AI creative tools promise to fix the production bottleneck of creative production every UA team is battling — but mostly, the output tanks your ROAS, or gets rejected by the network outright. For this session, Julien shares how his team actually integrates AI into their creative workflow, with real campaign data on what worked and what didn't.</p>
<p><strong>You’ll leave with: </strong>a production framework built for mobile performance constraints, rather than a list of prompts.</p>
<h3><strong>From competitor ad to finished creative, in one hour — Sven Jürgens, Mobile App Growth Consultant, Sven Juergens Consulting</strong></h3>
<p>Your competitors have already paid to find out what works in your category, and the results are sitting in a public database anyone can open. What are you waiting for?</p>
<p>Sven will share the whole process live: pulling a competitor's longest-running ads from the Meta Ad Library, lining up the first three seconds of each one until the pattern is obvious, then writing the brief and building the creative that answers it. Copying their ad might get you a worse version of theirs; but answering the pattern gets you yours.</p>
<p><strong>You’ll leave with: </strong>a one-page checklist of the nine steps to run this process on your own category the next day.</p>
<h3><strong>6+1 AI workflows to scale Meta from your phone — Marcus Burke, Meta Ads &amp; App Growth Advisor, Ctrl Shift</strong></h3>
<p>With AI, small and midsized teams can get much of the operating leverage that used to require a fully staffed marketing and growth team. In Marcus’s workshop, he’ll walk through the workflows used across real clients, all drawing on a shared memory that compounds as it learns your account, and how each one makes Meta easier to operate and scale.</p>
<p><strong>You’ll leave with:</strong> real workflows Marcus runs across client accounts.</p>
<h3><strong>Copy that converts: the language-market fit teardown — Lisa Kennelly, Growth and Product Marketing Strategist, Klarna</strong></h3>
<p>AI-written onboarding and paywall copy reads fine but converts flat — because nobody fed it real customer language. In Lisa’s session you’ll learn how to mine App Store reviews, support tickets, and cancellation reasons for the actual phrases, anxieties, and objections your users use, then watch a section of conversion copy rewritten live using what that turns up.</p>
<p><strong>You’ll leave with: </strong>a repeatable method for mining customer language going forward.</p>
<h3><strong>Building apps people want: the playbook behind 23 million organic downloads — Bria Sullivan, CTO &amp; Founder, Honey B Games</strong></h3>
<p>Most app developers build features nobody talks about. In this session, Bria will share the system she uses to decide what to build, and the process of making sure the ideas are compelling enough that users will tell their friends. From researching the market to running surveys, customer interviews, and testing visual directions on real people, this is Bria’s full workflow.</p>
<p><strong>You’ll leave with</strong>: a repeatable process for designing features around what your target users actually want.</p>
<h3>Yulia Lennox, Genesis Tech</h3>
<p>Details on the final workshop are coming soon, so stay tuned. </p>
<h2><strong>Other highlights</strong></h2>
<p>But App Growth Annual isn’t just about keynotes and workshops. Also on the agenda:</p>
<h3><strong>Who wants to be a millionARR?</strong></h3>
<p>The live interactive game show where app growth knowledge meets dramatic tie-breaker questions, phone-a-friend lifelines, and (of course) real buzzers. If you want to take part you’ll have to make it through the audience qualifier round, before beating fellow contestants in different categories to make it to the top of the leaderboard.</p>
<p>With questions covering app trivia, industry knowledge and growth benchmarks, we’d suggest you start polishing up your knowledge now… (<a href="https://www.revenuecat.com/state-of-subscription-apps/">The State of Subscription Apps</a> is good revision material!)</p>
<h3><strong>Exclusive virtual content</strong></h3>
<p>Online attendees get access to all the main stage content, streamed live from New York, but they <em>also </em>get to enjoy a whole hub of virtual-only material. Including: a networking breakout room, virtual Expo Hall to speak to RevenueCat and partners, and 1:1 speed dating-style networking.</p>
<p>Best of all, you can enter <strong>The Meowtrix</strong>, an interactive challenge for your App Growth Annual experience. Gather the most points among attendees by checking off specific side quests to climb the leaderboard and claim your prize.</p>
<h3><strong>The legendary afterparty</strong></h3>
<p>Last year saw Jason Paige (of Pokémon theme fame) bring the energy with a performance of <a href="https://www.youtube.com/watch?v=5rabIf__Ma8">RevenueCat’s official theme song</a>. We then had DJ Jazzy Jeff soundtrack the evening while folks networked, got airbrush tattoos, custom caricatures, and enjoyed plenty of delicious street food.</p>
<p>Will Jason Paige be back this year? Who knows. Will we play the RevenueCat theme song regardless? Probably. Will there be tasty food and free drinks? Of course!</p>
<p>And that’s our full lineup for App Growth Annual. A jam-packed schedule filled with insights, inspiration, and new connections — maybe you’ll even meet your future co-founder.</p>
<section><h3>Don’t miss out on app growth’s biggest event</h3><p>Register for free to watch every main-stage session, take part in online workshops, and participate in the virtual experience.</p><p><a href="http://appgrowthannual.com/virtual">Register for free</a></p></section>
<p>We’ll see you on October 21!</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[‘Not enough usage’: your biggest churn reason is a broken habit loop]]></title>
      <link>https://www.revenuecat.com/blog/growth/not-enough-usage-churn</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/not-enough-usage-churn</guid>
      <pubDate>Tue, 22 Sep 2026 12:03:00 GMT</pubDate>
      <dc:creator><![CDATA[Daphne Tideman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Diagnosing why users really leave (and no, it isn’t price-related)]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/1f5cc2f6f6afd5523589e910b410e4a0186d5e29-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Every January, I do a <a href="https://www.revenuecat.com/blog/growth/how-to-tackle-new-year-subscription-churn/">little subscription cull</a>. Consider it a bit of early app spring cleaning. The stretching app I swore I’d use daily but opened maybe three times? Gone. The language app a friend once recommended, now sitting there with a sad little four-day streak? Bye-bye. The new meditation app I was convinced would finally fix my sleep? Deleted.</p>
<p>None of them were canceled because they were bad or too expensive. They were canceled because, well… I wasn’t using them. Oops.</p>
<p>And it turns out I’m not alone.</p>
<p>According to<a href="https://www.revenuecat.com/state-of-subscription-apps-2025/"> the State of Subscription Apps 2025</a>, ‘not enough usage’ is the number one reason people cancel subscription apps, accounting for <strong>37% of cancellations</strong> — ahead of ‘price’ at 35%. <a href="https://www.revenuecat.com/blog/growth/subscription-app-trends-benchmarks-2026">The 2026 report</a> showed we got a little more price-sensitive, but ‘not enough usage’ is still the second biggest driver of churn;<strong> ranging from 26%–40%</strong>, depending on the category.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/20a8b558437980d366b827d7ce7987f483a7aaed-1758x864.png" alt=""/></figure>
<p>When I covered ‘not enough usage’ in my <a href="https://www.revenuecat.com/blog/growth/subscription-app-churn-reasons-how-to-fix">top five cancellation reasons article</a>, I gave it a short and sweet section. This has bothered me ever since. I haven’t seen a single article or podcast give this mega churn reason the attention it deserves. So here we go!</p>
<p>By the end of this read, you'll be able to tell the difference between a usage problem and a price problem. More importantly, you’ll know what to actually do about each issue.</p>
<aside class="tip"><strong>Before we start</strong><p>This is about voluntary usage churn, not the <a href="https://www.revenuecat.com/blog/growth/google-play-billing-error-churn-how-to-fix">involuntary billing kind</a>. Failed payments and expired cards are a completely different beast with a very different fix.</p></aside>
<h2>How to diagnose price vs. usage churn</h2>
<p>Here's the hard part: you often don’t know for sure why someone actually left. Even when you have a stated cancellation reason (Google Play surfaces these, while the App Store is a little trickier), cancellation surveys are not the reliable source of truth many teams treat them as.</p>
<p><a href="https://www.userintuition.ai/posts/why-your-exit-survey-is-lying-to-you-the-case-for-ai-moderated-churn-interviews/">A SaaS study on 723 participants by User Intuition</a> found that stated exit reasons matched the actual churn driver only 27.4% of the time. Price was the reason 34.2% of churners gave, but it was the real driver in just 11.7% of those cases. Whilst the study wasn’t focused specifically on mobile apps, the lesson still holds: <strong>the reason someone gives when they cancel is often the surface explanation, not the underlying cause</strong>.</p>
<p>“Too expensive” might actually mean “I wasn’t getting enough value.”</p>
<p>“Not using it enough” might actually mean “I stopped believing this would work for me.”</p>
<p>So don't stop at the survey data. Use quantitative signals to understand whether you’re dealing with a price problem, a value perception problem, or a usage problem. Look at things like engagement patterns, feature adoption, time-to-value, and retention cohorts.</p>
<p>Then layer in qualitative research (user interviews, cancellation conversations, and feedback from churned users) to understand the story behind the numbers.</p>
<h3>1. Define your version of ‘enough’ usage</h3>
<p>Before you can solve for usage or even start a lovely little quantitative deep dive, you need to know what ‘enough’ actually looks like for your app. And sometimes ‘enough’ is much less frequent than you expect. Sometimes it varies wildly between user groups.</p>
<p>I worked with a meditation and workshop app where some users opened it once every month or two and still happily paid for an annual subscription. Because the value they got from that single session was high enough to justify the cost.</p>
<p>Meanwhile, other users who opened the same app a few times a week didn’t think it was worth paying for. Same app, but completely different definitions of ‘enough’.</p>
<p>As Dan Layfield of <a href="https://subclub.com/episode/the-subscription-growth-formula-dan-layfield">Subscription Index</a> argued, usage cadence should match how long and <strong>how often the user actually experiences the problem you solve. </strong>A fitness app might need three sessions a week. A meditation app might need daily engagement. A personal finance tool might only need to be opened once or twice a month. <a href="https://www.revenuecat.com/glossary#daily-active-users-dau">Daily active users</a> are often the wrong <a href="https://www.revenuecat.com/blog/growth/activation-metrics/">metric</a> entirely.</p>
<p>This is exactly the point <a href="https://www.revenuecat.com/blog/growth/asya-paloni-welltory-sub-club-podcast">Asya Paloni of Welltory also makes</a>: Duolingo's daily streak and light-guilt mechanics work because the behavior is tiny — opening the app for three to five minutes gives the user an immediate reward. That model does not translate neatly to a behavior-change app where the user has to do something difficult in the real world.</p>
<p>Asya suggests that reminders and human support improve engagement in those contexts, while <a href="https://www.revenuecat.com/blog/growth/gamification-in-apps-complete-guide/">gamification</a> (especially shallow attempts like badges and streaks) often doesn’t improve <em>retention</em>. In some cases, forcing the Duolingo playbook onto a health or finance app can actually make things worse, creating pressure around the wrong behavior.</p>
<p>The classic activation thresholds are useful reference points:</p>
<ul>
<li>Slack found that around 2,000 messages correlated with teams that almost never churned</li>
<li>Facebook identified reaching 7 friends within 10 days.</li>
<li>Twitter/X focused on following 30 accounts.</li>
</ul>
<p>All widely reported. All completely different. All built around the natural cadence of a specific use case. So the question is: what’s yours?</p>
<aside class="tip"><strong>A useful exercise</strong><p>Look at what your long-term subscribers actually do in their first billing cycle. Not what they do once, or the flashy activation event. What behavior do they repeat? That recurring action, at whatever cadence makes sense for your product, is your version of <em>enough</em>.</p></aside>
<p>Then it’s time to start running the analyses that actually matter.</p>
<h3>2. Look at pre-cancellation usage</h3>
<p>Start with the 30 days leading up to cancellation for churned users. Were they still actively using the app? Or had usage already dropped to almost nothing?</p>
<p>If engagement had fallen off a cliff before they canceled, that’s a usage problem. The cancellation was just the final step.</p>
<p>If they were actively using the app right up until the moment they left, then price (or another factor) becomes a much more likely driver.</p>
<p>The key question: did they stop because they stopped seeing value, or did they stop because the price no longer felt justified? <a href="https://www.revenuecat.com/blog/growth/how-to-spot-churn-before-it-happens">This guide on spotting churn before it happens</a> covers the signals to look for in more detail.</p>
<h3>3. Segment churners by usage frequency</h3>
<p>Look at who is saying they left because the app was ‘too expensive’.</p>
<p>Are your heaviest users also citing price as a reason for leaving? If so, you may have a genuine pricing or value perception issue.</p>
<p>But if price complaints are concentrated among your lightest users, you might actually be looking at a usage problem wearing a price mask. They’re not thinking, “This costs too much”. They’re thinking, “I’m not getting enough out of this to keep paying”.</p>
<h3>4. Compare cancellation reasons across price tiers (if applicable)</h3>
<p>If you have multiple pricing tiers, compare churn reasons by plan. If your lowest-priced subscribers are still saying ‘not enough usage’ rather than ‘too expensive’ — especially if their actual usage data is low — the problem probably isn’t the price. The issue is that the product hasn’t become valuable enough in their routine.</p>
<p>Price is often the reason people say when the value equation stops making sense. The job is figuring out whether the problem is the number on the bill or the value on the other side of it.</p>
<h2>Why the discount reflex is the wrong response to churn</h2>
<p>What most teams do when they see ‘not enough usage’ in a cancellation survey is slap a discount on it. But think about it. If someone isn't using the app, why would they care what it costs? The issue usually isn’t that the app is too expensive. It’s that <strong>they’re not getting enough value.</strong> Lowering the price doesn’t fix that.</p>
<p>I saw this play out with a previous client. The cohort that signed up with a 50% discount had a £50 lower <a href="https://www.revenuecat.com/blog/growth/what-is-lifetime-value-ltv-apps">lifetime value (LTV)</a> and higher churn than the full-price sign-ups. Cheaper didn't fix usage, it made things worse.</p>
<p>A discount doesn’t repair any of those four points where a habit can break; it won’t create a trigger that brings someone back, or refresh a reward that stopped feeling valuable. So don’t try to win back usage churners with a price cut. <strong>Find where the habit is breaking, and fix that instead.</strong></p>
<h2>‘Not enough usage’ is actually just a broken habit</h2>
<p>The reason this category gets so little attention is that the label itself is a catch-all. What does ‘not enough usage’ actually mean? And is it just a softer version of ‘it’s too expensive’?</p>
<p>Does it mean that your subscriber:</p>
<ul>
<li>Forgot about your app? (How dare they!)</li>
<li>Used it for a bit and stopped? (But why?)</li>
<li>Lost interest in your app? (Ouch.)</li>
</ul>
<p>I quit my <a href="https://www.revenuecat.com/blog/growth/peloton-retention-takeaways/">Peloton subscription</a> last year. I never received a cancellation survey, but if I had, I would have selected ‘not enough usage’. And that was true; I wasn't using it enough anymore. But the real reason was that I'd fallen out of the habit because it had started to feel repetitive, and I wasn't seeing any difference in my workouts. <strong>‘Not enough usage’ was the symptom —</strong> the actual cause was my habit coming apart.</p>
<p>That word, ‘habit’, is the whole thing. Almost every version of ‘not enough usage’ comes down to:</p>
<ul>
<li>A habit never formed</li>
<li>A habit failed to deliver enough value</li>
<li>A habit quietly fell apart</li>
</ul>
<p>If you want to fix usage, you first need to understand <strong>how a habit actually holds together</strong>.</p>
<p><strong><a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/subscription-retention-chart">Nir Eyal's Hook Model</a></strong> is one of the clearest ways to think about habit loops:</p>
<ol>
<li>A trigger (a notification, a time of day, a feeling, or a problem they want to solve) brings someone back to your app</li>
<li>They take an action (use the app)</li>
<li>They receive a reward that feels worth the effort</li>
<li>They invest something (a preference, a history, a streak, personal data) that makes the next cycle easier and increases the cost of leaving.</li>
</ol>
<p>Repeat that loop enough times, and a habit forms. But remove the value from step three, and the loop collapses. You could argue that the reward is the most important part, and technically it is. But I think it goes deeper than that. You can create a short-term dopamine hit; a temporary reward, or a moment of engagement — but if the user doesn’t believe the app is delivering real value, the habit won’t survive. A habit isn’t built on activity alone. It’s built on <strong>the belief that coming back is worth it</strong>.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/917884d4cb165e15448c2582b8c95129172cd5f9-1172x924.png" alt=""/><figcaption>Credit: Nir Eyal’s Hook Model</figcaption></figure>
<p>Usage churn is this loop breaking. And in my experience, it tends to break at one of four predictable points.</p>
<h2>The trigger never fired</h2>
<p>Out of sight, out of mind. This happens right at the start, or with low-frequency apps, where people quietly drift back to whatever they used before because it's easier. This is the “I forgot about it” category.</p>
<p>You download a new recipe app, but you still end up opening Instagram or reaching for a dog-eared cookbook (guilty!). The novelty that made you download it faded before the app earned a place in your day, and once it's off the home screen, it's gone.</p>
<h3>How to fix a trigger that never fired</h3>
<p>Try a well-timed re-engagement sequence (<em>not</em> spray-and-pray notifications). Look at when usage typically drops during the billing cycle for your product, then meet users there with something genuinely useful.</p>
<p>I’ve seen this work directly: for users who had hit our definition of inactive (hadn’t completed a workout in five days), we sent a small number of targeted emails that acknowledged where they were and prompted them to re-engage. It drove a 100%+ lift in people completing a workout within three days, versus the control group who received no emails.</p>
<p>Focus on <strong>relevance </strong>and <strong>timing</strong>, not volume.</p>
<h2>The action never stuck</h2>
<p>Some people get going and still don't stick. The hardest part of a new habit is usually the beginning, when the effort is real, and the results are not visible yet. A five-minute journaling practice or a morning stretch feels like more work than payoff in week one. People start with good intentions, but then they slip once or twice, and never come back. This often starts with <a href="https://www.revenuecat.com/blog/growth/fix-onboarding-funnels">onboarding that's too short</a> to reach the moment where the app clicks.</p>
<p>The other half of sticking is finding a slot in the day for it, making it a part of your routine. My Spanish used to be decent. Then I left Spain after living there for a few months, stopped practicing, and it faded. During a trip to Ecuador, embarrassed by my Spanish and keen to pick it up again, I started using a language app: a lesson in the morning, another while waiting for the boat. But when I got home, I had no natural moment to keep it up, and I drifted. My older sister is the opposite. She practices French every night right before bed. Same time, same trigger, sticks to it religiously.</p>
<p>That is the difference between an action that sticks and one that doesn't: <strong>a trigger that fires on its own, not because of some push notification or email. </strong>Without one, daily becomes ad hoc, ad hoc becomes occasional, occasional becomes embarrassingly-bad Spanish. The user wasn't unmotivated. The action just never anchored to an <a href="https://www.revenuecat.com/blog/growth/solve-app-problems-emotionally/">emotional trigger</a>.</p>
<h3>How to fix it an action that doesn’t stick</h3>
<p>Try these two levers, depending on whether the user never activated or never anchored:</p>
<p>First: get them to the core action faster. The longer someone waits after paying <a href="https://www.revenuecat.com/blog/growth/hard-paywall-activation-journey">before they experience real value</a>, the less likely they are to ever form a habit around the product.</p>
<p>Second, borrow an existing trigger rather than trying to build one from scratch. The “Try it right after your morning coffee&quot; prompt, or the &quot;Try it before you sleep&quot; nudge: that anchor matters as much as the product itself. My sister's French habit holds because it's welded to her bedtime. Give users a way to weld by notifying them at the time they are most likely to take action.</p>
<h2>The reward went flat</h2>
<p>This is the one that most usage analyses miss, and the one I'd argue is entirely missing from most articles. Some users get all the way here. They built the routine, came back for a while, but then stop because the app ran out of reasons to pull them back.</p>
<p>Remember my <a href="https://www.revenuecat.com/blog/growth/peloton-retention-takeaways/">Peloton example</a>? I had the routine, and I'd felt the value, at first. But the workouts stopped surprising me: same format, same structure, nothing new to come back for. A reward that's identical every time stops being a reward; we need variation and excitement (especially this ADHDer).</p>
<p>This tends to be further down the user journey. Variability in rewards is key here; fresh content and visible progress are really powerful — these motivations are the app's responsibility, not the users’.</p>
<h3>How to fix it</h3>
<p>This is where variable rewards and investment earn their keep. The same reward every time gets boring. (Peloton again: same workouts, same format, no surprise, no fresh reason to come back.)</p>
<p>Vary your rewards, show progress, unlock rewards based on that progress; give people something new to come back for. Finch is a great example: offering a rotating daily shop, and seasonal rewards linked to repeated usage and habit-building.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/8fffe9c2312433112666bbc4484cd2307371dbb4-828x1792.jpg" alt=""/></figure>
<p>Also focus on building investment early: the more someone puts into an app — preferences, history, a profile — the more they lose by leaving. It’s the same sunk-cost fallacy that keeps us in relationships longer.</p>
<p>AI tools like <a href="https://www.granola.ai/">Granola</a> or <a href="https://wisprflow.ai/">Wispr Flow</a> are good examples. The more content I build up in them, the more useful they become, and the harder it would be to switch. Building that investment before the first renewal changes the value calculation.</p>
<h2>The value never landed, or never fit</h2>
<p>This one usually surfaces later. The user genuinely tried, but it just didn't feel like it was working. They used a language app for weeks and felt like the main word they learned was ‘apple’ (<em>manzana</em>, in my experience with Duolingo). Value can take longer than people expect, or the outcome the app delivers may be different from what they wanted.</p>
<p>The pattern looks like this: you journal for a month and feel no different, so you stop. You might not cancel immediately because part of you thinks, “maybe I’ll get back into it.” Then renewal comes around and suddenly the decision feels obvious: “I’m not using it enough. Might as well cancel.”</p>
<p>Technically, that’s true. But ‘not enough usage’ was the symptom. The root cause was a value gap.</p>
<p>There's a quieter, less obvious reason worth separating out, because the fix is completely different:<strong> the app works; it just doesn't fit their situation.</strong> A meal-planning app that assumes you cook most nights might be genuinely excellent. But for someone who eats out five times a week, it feels useless or unnecessary.</p>
<p>The value isn’t slow to arrive; rather, the product simply doesn’t match the reality of the person using it. When this happens, no reminder, notification, or re-engagement campaign can fix that (no matter how good they are). And sometimes that’s okay. Some churn isn’t a failure to prevent; it’s the right decision for the user.</p>
<h3>How to fix it</h3>
<p>Surface outcomes and progress, not just features. Most apps show what they do. Few show what the user has actually achieved. A monthly progress summary, a milestone email, a reminder of how far someone has come. Those moments are what shift someone from “I’m not sure this is working” to “actually, look at that.” Take Loom’s monthly recap emails — they literally show you the value in one image:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/bda4f3c5ce9206b4e9c1592096ad75f632696ec8-1400x994.jpg" alt=""/></figure>
<p>For users who've already dropped the app, your typical <a href="https://www.revenuecat.com/blog/growth/app-reactivation-strategy-how-to/">reactivation playbook</a> applies, though reactivation rates are low by default, which is exactly why catching the value gap earlier matters so much. And if it's a life-fit problem rather than a delivery problem, be honest with yourself: no email can surface value that was never going to exist for that user. <strong>Qualifying the right people beats winning back the wrong ones</strong>.</p>
<aside class="tip"><strong>A practical note on sequence</strong><p>These breaks tend to cluster by stage. The trigger and action problems usually appear early, often in the first billing cycle. A flat reward shows up a little later, once the novelty's gone. The value break can surface at any point, but it tends to be why long-term users eventually drift. If your early numbers are soft, start at the trigger and the action, then work your way around the loop.</p></aside>
<p>Each break sits at a different point on the loop, and each needs a different fix. Not one of them responds to a discount. But before you reach for a fix, you need to know which break you're actually looking at and make sure you aren’t dealing with a price problem.</p>
<h2>How to know which part of the habit loop is broken</h2>
<p>You've established the churn is coming from usage, not price. Now you need to figure out which of the four breaks you’re dealing with. Luckily, each one leaves a different fingerprint on the usage curve.</p>
<p>Pull your churned cohorts and find the moment users actually went inactive (not when they canceled, but when they genuinely stopped coming back). Then look at the shape of the drop-off:</p>
<table>
<thead><tr>
<th><p><strong>Drop off</strong></p></th>
<th><p><strong>What’s broken in the habit loop</strong></p></th>
</tr></thead>
<tbody>
<tr>
<td><p>They barely used the app from the start and disappeared after the first session or two</p></td>
<td><p>The <strong>trigger never fired</strong>: the user never built a reason to return</p></td>
</tr>
<tr>
<td><p>They started, used it a handful of times, then gradually tailed off before it became a routine (often within the first few weeks)</p></td>
<td><p>The <strong>action never stuck</strong>: the behavior was too much effort, too unclear, or didn’t become part of their routine</p></td>
</tr>
<tr>
<td><p>They used it consistently for a while, then engagement slowly declined once the novelty wore off</p></td>
<td><p>The <strong>reward went flat</strong>: the app delivered enough initial value to create a habit, but not enough ongoing value to maintain one.</p></td>
</tr>
<tr>
<td><p>They kept showing up, but the outcome never changed, or usage was always sporadic because their situation didn’t really require frequent use</p></td>
<td><p>The <strong>value never landed</strong>, or the product never truly fit their life</p></td>
</tr>
</tbody>
</table>
<p>The usage curve will tell you where to look, but don’t stop there — to understand why the curve looks the way it does, you need to talk to churned users. The data can show you when the habit broke, but conversations tell you <em>why</em>.</p>
<h3>Talk to churned users (and what to do if you can't)</h3>
<p>The most direct way to find out why a habit broke is to talk to the people who left. It’s the equivalent of asking for directions when you’re lost.</p>
<p>Ask users two things:</p>
<ol>
<li>What did they expect from the app?</li>
<li>What did they actually experience?</li>
</ol>
<p>That gap is almost always where usage broke, and it usually tells you which break it was. Then ask what was going on in their life around the time they drifted, especially if you can see usage drop before they canceled.</p>
<p>I know churned users are harder to reach. I know they don't always reply. But trust me, it's worth it.</p>
<p>If you genuinely can't get to your churned users, shorten the feedback loop instead. <a href="https://subclub.com/episode/finding-product-market-fit-by-unbundling-photoshop-matthieu-rouif-photoroom">Matthieu Rouif of PhotoRoom deliberately launched with a monthly-only plan</a> so users would churn faster and become interviewable sooner. If you're early-stage and your usage understanding is low, that trade-off is worth considering.</p>
<p>Other proxies when you can't reach users directly:</p>
<ul>
<li>Support tickets</li>
<li>App store reviews</li>
<li>More in-depth post-cancellation surveys</li>
</ul>
<p>They might not tell you the exact cause 100% of the time, but they can help you narrow down where to look.</p>
<h2>Fix the right problem</h2>
<p>‘Not enough usage’ can sound vague, but once you dig into the quantitative and qualitative data, it usually isn’t. At its core, it comes down to a habit loop that never fully formed, due to one of four breaks: the trigger, the action, the reward, or the value.</p>
<p>And none of those problems are solved by the classic <a href="https://www.revenuecat.com/blog/growth/win-back-customers-how-to-guide">winback</a> discount. They require a better diagnosis and a better strategy.</p>
<p>Start by understanding what ‘enough usage’ actually looks like for your product, then run the quantitative checks I outlined. From there, work backward: where are users dropping off, and why?</p>
<p>Fixing this mega churn reason is a journey, but it’s worth taking the time to get it right. When an app delivers real value, users become far less sensitive to both price and usage. The value they get in the moment is enough to justify coming back, and enough to justify staying.</p>
<p>Usage churn is harder to fix than a pricing problem, but it’s also the lever almost nobody pulls. And that’s exactly why the opportunity is so wide open.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Seven more ways to win Shipaton]]></title>
      <link>https://www.revenuecat.com/blog/company/seven-more-ways-to-win-shipaton</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/seven-more-ways-to-win-shipaton</guid>
      <pubDate>Fri, 18 Sep 2026 12:42:28 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[You may not need another app. One useful integration, growth experiment, or sharper audience could put the app you're already building in contention for another prize.
]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/3aa26c27cbd3199b39e2e3e9aac3bb620ecf08d4-1600x818.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Shipaton has a lot of prizes. Enough that it's easy to pick a headline category, get back to building, and forget to look at the rest. I'd understand if you haven't studied every row of the prize table.</p>
<p>To help you out, I went through the categories again, and collected seven that you should consider targeting during the remaining two weeks. All of these reward work that would make your app stronger anyway: finding a route to market, testing growth, launching to a new market, or getting clearer about your audience.</p>
<p>Some can fit an app you already have in progress, and there’s still time to pivot or even build a new app.</p>
<h2>Launch a web-to-app funnel for the Funnel Vision Award</h2>
<p>If your app is nearly done, the next question is awkward but important: how will anyone find it?</p>
<p>The Funnel Vision Award gives you a reason to answer that now (that plus the $15,000). To enter, launch a live web-to-app funnel with <a href="https://www.revenuecat.com/docs/tools/funnels">RevenueCat Funnels</a> and use Stripe for checkout. Best thing: you most likely don’t need to release a new version of your app at all.</p>
<p>Keep the first version laser-focused. Pick one audience, one promise, and one offer. Send people to the funnel from a post or small ad campaign, then watch where they continue, pay, or drop off. The judges will look at payment volume, funnel design, and conversion. So don’t build a generic landing page that could belong to any app, but instead make the journey feel like the first useful part of your product.</p>
<p>Personally, I’d spent the least amount of time on the visuals and more time making the all the steps serve a purpose. The app that understands why someone clicked has a much better shot at converting them. If web acquisition is new to you, the <a href="https://www.revenuecat.com/guides/web-to-app-funnels">Web-to-App Funnels Handbook</a> covers pretty much everything.</p>
<h2>Take an idea all the way to income with Replit</h2>
<p>The Idea to Income Award offers $15,000 for first place. And it’s exactly what the name suggests: start with an idea, use Replit to build it, publish it, and prove that somebody will pay for it. You’ll need to build the mobile app with Replit, integrate RevenueCat using Replit Agent, and share at least three public posts from different points in the build.</p>
<p>To make the best use of your time, build for iOS, with that you have the biggest chance to get your app approved in time. The scope can be small (and should be). A specialist calculator, a training companion for one sport, or a practical tool for one profession can become a real product faster than an app trying to serve everyone.</p>
<h2>Give your Android app a Galaxy-specific advantage</h2>
<p>Already building for Android? Publishing on the Galaxy Store opens another route into Shipaton — and another market for customers to find you. Galaxy store optimization accounts for 20% of the score, and the store listing itself needs to be polished. A thing you can work on without having to release a new version of your app.</p>
<p>Should you target Samsung products by focusing on their unique selling points, that is also possible. Look at the hardware your app will run on. For example a foldable can turn a recipe app into ingredients on one side and instructions on the other. A planning app can keep the calendar and task details visible together. Small updates you can make to your app to make it more suited for Samsung devices.</p>
<p>You don’t need to use every Samsung-specific feature. Pick the one that makes sense for your app and make it feel intentional.</p>
<p>The prize is three weeks of featured placement across the Galaxy Store’s Apps and Discover tabs. There’s no cash prize here, but for an app that’s ready to convert and retain users, the distribution could bring even more money.</p>
<h2>Run one honest growth experiment with Layers</h2>
<p>You don’t need a huge audience to enter the Growth Loop Award. You need a clear idea of who the app is for, a reason they might care, and an experiment you can learn from. That makes this a good category for a newly launched app, and the winner takes $15,000.</p>
<p>Install the Layers SDK, choose one growth loop , and write down what you expect to happen. Then run it long enough to see a signal. If this sounds foreign to you, Layers has good resources on how to do this, and while going through those you also learn how to market your app, something you should do even if you didn’t target this prize category.</p>
<p>The result you get doesn’t have to be a hockey stick. The judges want to see that you treated growth as a loop: form a hypothesis, run the test, and then decide what comes next.</p>
<h2>Make today’s movement decision easier for Simone Sharice’s audience</h2>
<p>The Simone Sharice Influencer Award (with $20,000 for first place) asks for a personalized wellness companion covering movement, Pilates, recovery, and other day-to-day wellness needs. This does not mean that you need to build an endless workout library and ask users to figure it out. Better to build something personalized; a plan they can actually start.</p>
<p>To give you an example, on a low-energy day, that may be 10 minutes of recovery work. On another, it may be a focused Pilates session. The value is in making the choice feel manageable. So personalization matters, but so does restraint. The brief specifically rewards apps that avoid overload and make the next action obvious.</p>
<p>A strong first version could do three things well: understand today’s context, recommend a suitable session, and learn enough from the result to improve tomorrow’s recommendation.There are multiple Shipaton submissions targeting health and fitness, if yours is one, check if you could submit it for this award.</p>
<h2>Build a nutrition app that doesn’t turn lunch into homework</h2>
<p>Most nutrition apps start with numbers. Abbey’s Kitchen brief starts somewhere more human: help people make meals more satisfying without calorie counting or macro tracking.</p>
<p>Your app could let someone photograph lunch and ask what would make it more filling or enjoyable. It could let customers turn leftovers into a workable meal, or suggest useful additions based on what’s already in the kitchen. Something that is missing completely from current calorie and meal tracking apps. </p>
<p>You can build the core around one simple question: “What can I add?” Answer it well, in the moment when someone is hungry and short on ideas, and you’ve built something that is perfect for Abbey’s Kitchen’s massive audience.</p>
<h2>Let new managers rehearse the conversation they’re avoiding</h2>
<p>New managers are expected to give feedback, hold boundaries, and say no. These are often things you don’t get to practise beforehand. That’s the opening behind the Leadership Heather Influencer Award, which also offers $20,000 for first place.</p>
<p>What you could build is for example a rehearsal room, giving the user a realistic situation, letting them respond by voice or text, and then offer feedback they can use on the next attempt.</p>
<p>Start with a handful of conversations people genuinely dread:</p>
<ul>
<li>Giving direct feedback to a strong performer whose behaviour is negatively affecting the team</li>
<li>Saying no to an unrealistic request from a stakeholder</li>
<li>Setting a boundary with a direct report</li>
</ul>
<p>Build it so that users of the app can get personalized feedback. Did the manager state the issue clearly? Did they bury the message? Did they set a boundary? Did they leave room for the other person to respond? All of these are very easy to codify in such a way that LLM’s can answer and give personalized feedback.</p>
<h3>A quick note on the Influencer Awards</h3>
<p>These categories give you an audience and a problem to solve. They don’t give you permission to use someone else’s identity. Name the Influencer Award in your submission and explain how the app serves that audience. But don’t use the influencer’s name, image, likeness, voice, branding, or logos in the product or marketing without written consent.</p>
<p>You can enter each project in only one Influencer Award. The same project can also compete in other non-Influencer categories when it meets their requirements.</p>
<h2>Make things easy for the judges</h2>
<p>Each award has its own requirements: a live funnel URL, Stripe Project ID, Replit preview URL, public build posts, Galaxy Store listing, verified Layers installation, or an explanation of how you served the chosen audience. It’s crucial that you don’t make judges hunt for that information in your submission.</p>
<p>The rest of your submission matters too. You’ll need a live store listing, a demo video under two minutes, an app icon, the required screenshot, and a free trial or promo code that unlocks paid features for judging. Our <a href="https://www.revenuecat.com/blog/engineering/how-to-submit-your-app-for-shipaton">Shipaton submission guide</a> walks through each of these requirements so take a look at it before you submit.</p>
<p><strong>Shipaton closes on September 30, 2026 at 11:45pm PDT</strong>. Store review can take days, so ship the smallest valuable version now and improve it while the review is ongoing. For guidance on submitting your app for review, see our <a href="https://www.revenuecat.com/blog/company/the-late-submitters-guide-to-getting-through-app-review">Late submitter’s guide</a>.</p>
<p><a href="https://revenuecat-shipaton-2026.devpost.com/rules">Read the full rules and enter Shipaton 2026.</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Custom Purchase Flows and Paywalls with RevenueCat on Android]]></title>
      <link>https://www.revenuecat.com/blog/engineering/custom-flows-android</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/custom-flows-android</guid>
      <pubDate>Fri, 18 Sep 2026 05:45:39 GMT</pubDate>
      <dc:creator><![CDATA[Jaewoong Eum]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[In this article, you'll explore building a custom paywall against the Purchases SDK.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/42a58b25733f00d9a57645e280a2069bd64a5ec5-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>RevenueCat's prebuilt <code>Paywall</code> composable handles the common case in one line, and for most apps that is where the story ends. But two questions come up over and over: how to run your own code when a purchase succeeds or fails, and how to control what Google Play does when a subscriber switches plans. Both answers live in builders, and both are easy to get wrong because the defaults are silent. A subscription upgrade that charges the wrong amount looks exactly like one that charges the right amount until a user complains.</p>
<p>In this article, you'll explore building a custom paywall against the Android <code>Purchases</code> SDK, every option on <code>PurchaseParams.Builder</code>, the five replacement modes that govern plan changes, purchase results in callback and coroutine form, and the customization surfaces on RevenueCat's own paywall UI: <code>PaywallOptions</code>, <code>PaywallListener</code>, <code>PaywallActivityLauncher</code>, and <code>PaywallPurchaseLogic</code>.</p>
<h2><strong>Two decisions, not one</strong></h2>
<p>Before any code, separate two things that get conflated. Who owns the UI and who completes the transaction are independent choices, and the SDK lets you mix them.</p>
<ul>
<li><strong>Your own UI, RevenueCat completing the purchase</strong>: <code>PurchaseParams</code> and <code>Purchases.purchase</code>.</li>
<li><strong>RevenueCat's paywall UI, RevenueCat completing the purchase</strong>: <code>PaywallOptions</code> and <code>PaywallListener</code>.</li>
<li><strong>RevenueCat's paywall UI, your own code completing the purchase</strong>: <code>PaywallOptions</code> and <code>PaywallPurchaseLogic</code>.</li>
<li><strong>Your own UI, your own code completing the purchase</strong>: your own <code>BillingClient</code> calls, followed by <code>syncPurchases</code>.</li>
</ul>
<p>Most people asking how to customize the purchase flow want one of the first two. They both let RevenueCat complete the transaction and differ only on who draws the screen. This article covers those, plus the third, where your own code completes the Google Play transaction. The fourth is a migration topic rather than a customization one and is out of scope here.</p>
<p>You choose who completes the transaction at configuration time:</p>
<pre><code>val configuration = PurchasesConfiguration.Builder(context, apiKey)
    .purchasesAreCompletedBy(PurchasesAreCompletedBy.REVENUECAT)
    .build()

Purchases.configure(configuration)</code></pre>
<p><code>PurchasesAreCompletedBy.REVENUECAT</code> is the default and means the SDK acknowledges verified purchases for you. <code>PurchasesAreCompletedBy.MY_APP</code> means it does not, and that obligation has a deadline covered in the last section. The older <code>observerMode(true)</code> setter still exists but is deprecated in favor of this one.</p>
<h2><strong>Reading an offering into your own UI</strong></h2>
<p>A custom paywall starts by fetching offerings and rendering whatever the dashboard returned. The callback form takes a lambda pair:</p>
<pre><code>Purchases.sharedInstance.getOfferingsWith(
    onError = { error -&gt; _uiState.value = PaywallUiState.Error(error.message) },
    onSuccess = { offerings -&gt;
        val current = offerings.current
        if (current == null) {
            _uiState.value = PaywallUiState.Error(&quot;No current offering&quot;)
        } else {
            _uiState.value = PaywallUiState.Success(current)
        }
    },
)</code></pre>
<p><code>offerings.current</code> is nullable, and an app that assumes otherwise crashes the first time someone forgets to mark an offering as current in the dashboard. From there, <code>offering.availablePackages</code> is the list you render. Each <code>Package</code> carries an <code>identifier</code>, a <code>packageType</code>, and the <code>product</code> itself.</p>
<p>There is one nullable accessor per standard package type, <code>offering.monthly</code>, <code>offering.annual</code>, <code>offering.weekly</code>, <code>offering.lifetime</code>, and so on for each <code>PackageType</code>. Lookup by identifier behaves differently and catches people out:</p>
<pre><code>val monthly: Package? = offering.monthly
val custom: Package = offering.getPackage(&quot;my_custom_package&quot;)</code></pre>
<p><code>getPackage</code> returns a non-null <code>Package</code> and throws <code>NoSuchElementException</code> when the identifier is absent, as does the <code>offering[&quot;my_custom_package&quot;]</code> operator form. Wrap it or use the nullable accessors.</p>
<p>For the price label, read through to the product:</p>
<pre><code>val Package.buttonText: String
    get() = with(product) {
        if (type == ProductType.SUBS) {
            &quot;${price.formatted} for ${period?.value} ${period?.unit?.name?.lowercase()}&quot;
        } else {
            &quot;${price.formatted} one time&quot;
        }
    }</code></pre>
<p><code>price.formatted</code> is the localized string Google Play returned, already carrying the right currency symbol and grouping for the user's storefront. <code>price.amountMicros</code> and <code>price.currencyCode</code> are there when you need to compute something. <code>period</code> is returned only for Google subscriptions, so it is null for one time products and for Amazon.</p>
<h3><strong>Why there is no eligibility check on Android</strong></h3>
<p>On iOS there is an eligibility check you call before showing a trial badge. On Android there is not, and looking for one is a common dead end. <code>getEligibleWinBackOffers</code> and <code>checkTrialOrIntroDiscountEligibility</code> do not exist in the Android SDK. Google Play bakes eligibility into what it returns: an offer the user is not eligible for is not in <code>subscriptionOptions</code> at all.</p>
<p>So you read the offers you were given. Two container types are involved and the naming is close enough to confuse. <code>SubscriptionOptions</code> is the collection of offers on a product, and each <code>SubscriptionOption</code> is one offer made of pricing phases. Accessors on the collection pick an offer, accessors on an option pick a phase:</p>
<pre><code>val options = packageToPurchase.product.subscriptionOptions
val trialOption = options?.freeTrial
val introOption = options?.introOffer
val defaultOption = packageToPurchase.product.defaultOption</code></pre>
<p><code>SubscriptionOptions</code> implements <code>List&lt;SubscriptionOption&gt;</code>, so you can iterate it directly. On an individual option, the phase accessors are <code>freePhase</code>, <code>introPhase</code>, and <code>fullPricePhase</code>. There is no <code>freeTrialPeriod</code> on a Google product. That property exists only on <code>AmazonStoreProduct</code>. A single option can carry both a free phase and an intro phase.</p>
<p>To show what a multi-phase offer costs over time, join the phases:</p>
<pre><code>val pricingText = option.pricingPhases.joinToString(separator = &quot; then &quot;) { phase -&gt;
    &quot;${phase.price.formatted} for ${phase.billingPeriod.iso8601}&quot;
}</code></pre>
<p>You might reach for <code>phase.offerPaymentMode</code> to label each phase instead of formatting the price. It is worth knowing its shape before you do. The type is <code>OfferPaymentMode?</code>, one of <code>FREE_TRIAL</code>, <code>SINGLE_PAYMENT</code>, or <code>DISCOUNTED_RECURRING_PAYMENT</code>, and it is null for any phase that is not <code>FINITE_RECURRING</code>. That includes the full price phase of every recurring subscription and every phase of a prepaid plan. So branch on it with a <code>when</code> that treats null as the ongoing price rather than as an error.</p>
<h2><strong>Making the purchase with </strong><code><strong>PurchaseParams.Builder</strong></code></h2>
<p>Every purchase goes through <code>PurchaseParams</code>. Its builder has exactly three public constructors, and which one you pick decides how much control you have over the offer:</p>
<ol>
<li><code>PurchaseParams.Builder(activity, packageToPurchase)</code> takes a <code>Package</code>.</li>
<li><code>PurchaseParams.Builder(activity, storeProduct)</code> takes a <code>StoreProduct</code>.</li>
<li><code>PurchaseParams.Builder(activity, subscriptionOption)</code> takes a specific <code>SubscriptionOption</code>.</li>
</ol>
<p>The first two do not purchase the base plan. They purchase the product's <code>defaultOption</code>, and the builder KDoc spells out how that is chosen: offers tagged <code>rc-ignore-offer</code> or <code>rc-customer-center</code> are filtered out, then the option with the longest free trial or the cheapest first phase wins, then it falls back to the base plan. That is usually what you want, and it is why most apps never touch <code>SubscriptionOption</code> at all.</p>
<p>Pass a <code>SubscriptionOption</code> when you need to override that choice, for example when your paywall shows two offers side by side and the user picked the one that is not the default:</p>
<pre><code>val params = PurchaseParams.Builder(activity, selectedOption).build()</code></pre>
<p>The simplest possible purchase is the package form with nothing else set:</p>
<pre><code>Purchases.sharedInstance.purchaseWith(
    PurchaseParams.Builder(activity, packageToPurchase).build(),
    onError = { error, userCancelled -&gt;
        if (!userCancelled) showError(error)
    },
    onSuccess = { _, customerInfo -&gt;
        unlockContent(customerInfo)
    },
)</code></pre>
<p>Two things about that snippet need stating outright. First, the <code>onSuccess</code> transaction parameter is typed <code>StoreTransaction?</code>, nullable, unlike the non-null <code>StoreTransaction</code> you get from the <code>PurchaseCallback</code> interface. Writing <code>onSuccess = { transaction, _ -&gt; log(transaction.orderId) }</code> does not compile. Discard it with <code>_</code> or use <code>transaction?.orderId</code>.</p>
<p>Second, <code>userCancelled</code> is not extra information. It is derived, in <code>PurchasesOrchestrator</code>, from a single comparison:</p>
<pre><code>onError(
    error,
    error.code == PurchasesErrorCode.PurchaseCancelledError,
)</code></pre>
<p>So checking the boolean and checking the code are equivalent. Either way, a user tapping outside the Google Play sheet is not an error you should surface. Showing a toast that says &quot;Purchase failed&quot; because someone changed their mind is a common defect in hand rolled paywalls.</p>
<h3><strong>The callback interface</strong></h3>
<p><code>purchaseWith</code> is a Kotlin extension. The interface form takes a <code>PurchaseCallback</code>, which declares <code>onCompleted</code> and inherits <code>onError(error, userCancelled)</code> from <code>PurchaseErrorCallback</code>, so you implement two methods:</p>
<pre><code>Purchases.sharedInstance.purchase(
    purchaseParams = params,
    callback = object : PurchaseCallback {
        override fun onCompleted(storeTransaction: StoreTransaction, customerInfo: CustomerInfo) {
            unlockContent(customerInfo)
        }

        override fun onError(error: PurchasesError, userCancelled: Boolean) {
            if (!userCancelled) showError(error)
        }
    },
)</code></pre>
<p>Here <code>onCompleted</code> hands you a non-null <code>StoreTransaction</code>, which is the reason to prefer this form when you need the purchase token. Note that <code>purchaseToken</code> is non-null but <code>orderId</code> is still <code>String?</code> even here, because it is absent for pending and test purchases.</p>
<h3><strong>Coroutines</strong></h3>
<p>The suspend variants come in two flavors per operation, one that throws and one that returns <code>Result</code>:</p>
<pre><code>viewModelScope.launch {
    try {
        val result = Purchases.sharedInstance.awaitPurchase(params)
        unlockContent(result.customerInfo)
    } catch (e: PurchasesTransactionException) {
        if (!e.userCancelled) {
            showError(e.error)
        }
    }
}</code></pre>
<p><code>awaitPurchase</code> returns a <code>PurchaseResult</code> holding <code>storeTransaction</code> and <code>customerInfo</code>. That type gets its <code>equals</code>, <code>hashCode</code>, and <code>toString</code> from the Poko compiler plugin rather than from <code>data class</code>, which means there is no generated <code>copy()</code> and no destructuring. Read the two properties directly.</p>
<p><code>PurchasesTransactionException</code> is where <code>userCancelled</code> lives on the coroutine path, and it exposes <code>error</code>, <code>code</code>, and <code>underlyingErrorMessage</code>. That last one matters for support, because <code>PurchasesError.message</code> is not a server message. It is a computed property returning <code>code.description</code>, the same canned English sentence for every occurrence of a given code. <code>underlyingErrorMessage</code> carries what actually went wrong, so log both.</p>
<h2><strong>Reading the result</strong></h2>
<h3><strong>Entitlements</strong></h3>
<p>The <code>CustomerInfo</code> handed to your success callback is already up to date, so gate on it directly rather than issuing another fetch. Watch which collection you read from. <code>entitlements.all</code> contains every entitlement the user has ever had, including expired ones, while <code>entitlements.active</code> contains only the live ones. Reading <code>all</code> and forgetting to check <code>isActive</code> grants access to a lapsed subscriber.</p>
<pre><code>val isPro = customerInfo.entitlements[&quot;pro&quot;]?.isActive == true
val hasAnything = customerInfo.entitlements.active.isNotEmpty()</code></pre>
<p>The indexing operator reads through <code>all</code>, which is why the <code>isActive</code> check is not optional there. <code>entitlements.active</code> is a <code>Map&lt;String, EntitlementInfo&gt;</code>, not a list, so <code>active.keys</code> gives you the identifiers.</p>
<p>Individual <code>EntitlementInfo</code> properties worth surfacing in a settings screen:</p>
<ul>
<li><code><strong>willRenew</strong></code>: false once the user has turned off auto renew. Always true for lifetime access.</li>
<li><code><strong>expirationDate</strong></code>: null for lifetime access. For a trial, this is the trial expiration.</li>
<li><code><strong>periodType</strong></code>: one of <code>NORMAL</code>, <code>INTRO</code>, <code>TRIAL</code>, <code>PREPAID</code>.</li>
<li><code><strong>unsubscribeDetectedAt</strong></code>: when the user cancelled, if they did.</li>
<li><code><strong>billingIssueDetectedAt</strong></code>: when a payment problem started, if there is one.</li>
</ul>
<p>The KDoc on the last two carries the important part. An entitlement can still be active while both are set, so branch on <code>isActive</code> for access control and treat the dates as context for messaging.</p>
<p>To react to changes that happen outside your purchase call, set the listener once and clear it:</p>
<pre><code>Purchases.sharedInstance.updatedCustomerInfoListener = UpdatedCustomerInfoListener { info -&gt;
    _customerInfo.value = info
}</code></pre>
<p>This fires when the SDK updates its cache after an app launch, a purchase, a restore, or a fetch. It is not a server push, so it is not a substitute for calling <code>getCustomerInfo</code> when your UI needs current state.</p>
<h3><strong>Restore</strong></h3>
<p>Restore is a single call with the same two shapes, and it is not optional. Both stores require a way for a user on a new device to recover access:</p>
<pre><code>Purchases.sharedInstance.restorePurchasesWith(
    onError = { error -&gt; showError(error) },
    onSuccess = { customerInfo -&gt; refreshUi(customerInfo) },
)</code></pre>
<p>One naming inconsistency to watch for when you implement the interfaces instead: <code>ReceiveCustomerInfoCallback</code> uses <code>onReceived</code>, while <code>SyncPurchasesCallback</code> uses <code>onSuccess</code>.</p>
<h2><strong>Switching plans: Product changes and replacement modes</strong></h2>
<p>Everything above buys a new subscription. Moving an existing subscriber from one plan to another is a different operation, and it needs two more builder calls:</p>
<pre><code>val params = PurchaseParams.Builder(activity, annualPackage)
    .oldProductId(currentSubscriptionId)
    .replacementMode(StoreReplacementMode.CHARGE_PRORATED_PRICE)
    .build()</code></pre>
<p>Product changes work in the Play Store and the Galaxy Store. The Amazon Appstore ignores them.</p>
<h3><strong>Getting </strong><code><strong>oldProductId</strong></code><strong> right</strong></h3>
<p>The value is the subscription id, not the id plus base plan. This trips people up because the place you naturally read it from returns the combined form:</p>
<pre><code>val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
val oldProductId = customerInfo.activeSubscriptions
    .map { it.split(&quot;:&quot;).first() }
    .firstOrNull()</code></pre>
<p><code>activeSubscriptions</code> returns <code>subscriptionId:basePlanId</code> for Google subscriptions. The builder is forgiving here, its KDoc says anything after a <code>:</code> is ignored, but splitting yourself makes the intent visible.</p>
<h3><strong>The five replacement modes</strong></h3>
<p><code>StoreReplacementMode</code> replaced <code>GoogleReplacementMode</code> in SDK 10.3.0. The old enum and the old <code>.googleReplacementMode()</code> setter both still exist and are both deprecated, so new code should use <code>StoreReplacementMode</code> and <code>.replacementMode()</code>.</p>
<ul>
<li><code><strong>WITHOUT_PRORATION</strong></code>: takes effect immediately. The user pays nothing today, then the full new price on the old plan's expiration date.</li>
<li><code><strong>WITH_TIME_PRORATION</strong></code>: takes effect immediately. The user pays nothing today, and the time remaining on the old plan becomes free time on the new one.</li>
<li><code><strong>CHARGE_PRORATED_PRICE</strong></code>: takes effect immediately. The user pays the price difference today and the billing date does not move.</li>
<li><code><strong>CHARGE_FULL_PRICE</strong></code>: takes effect immediately. The user pays the full new price today and the remaining time from the old plan is credited forward.</li>
<li><code><strong>DEFERRED</strong></code>: takes effect when the old plan expires. The user pays nothing today, then the new price on the old expiration date.</li>
</ul>
<p>One structural note if you plan to branch on the mode you were given. <code>StoreReplacementMode</code> is not an enum, it is a class exposing five constants, so a <code>when</code> over it will not compile as exhaustive. Include an <code>else</code> branch.</p>
<p>The default is <code>WITHOUT_PRORATION</code>. It is set in the builder's internal state, not left null, so a product change with no <code>replacementMode</code> call is a real decision that was made for you. For a paid upgrade that is rarely the one you want, because the user gets the better plan immediately and pays nothing until their old period ends.</p>
<p>If you want to have a better undertsanding on the proration mode, check out <a href="https://www.revenuecat.com/blog/engineering/google-proration">Understanding Google Play subscription proration: a developer’s guide</a>.</p>
<h3><strong>Choosing a mode</strong></h3>
<ul>
<li><strong>Upgrade, charge now</strong>: <code>CHARGE_PRORATED_PRICE</code>. The user pays the difference and the renewal date does not move. This mode is only valid when the new plan's price per time unit is higher than the old one's, which Google Play checks and rejects otherwise, so it is an upgrade only mode rather than a general purpose one.</li>
<li><strong>Upgrade, generous</strong>: <code>WITH_TIME_PRORATION</code>. The user pays nothing today and their unused time becomes free days on the higher tier.</li>
<li><strong>Downgrade</strong>: <code>DEFERRED</code>. The user keeps what they paid for until it runs out, then drops to the cheaper plan. Charging someone immediately to receive less is a support ticket.</li>
<li><strong>Crossgrade at the same price</strong>: <code>WITH_TIME_PRORATION</code> or <code>DEFERRED</code>, depending on whether the switch should be felt now.</li>
</ul>
<p>Two constraints come straight from the KDoc and are worth encoding as guards. <code>WITH_TIME_PRORATION</code> fails on the Play Store when both options belong to one <code>StoreProduct</code>, so use a different mode for base plan changes within a single product. <code>CHARGE_FULL_PRICE</code> is not supported by the Galaxy Store, and passing it there surfaces a <code>PurchasesErrorCode.UnsupportedError</code> in your error callback rather than starting a purchase.</p>
<h3><code><strong>DEFERRED</strong></code><strong> reports back differently</strong></h3>
<p>The deferred mode is the one that makes purchase handling code look broken. Nothing changes today, so the <code>CustomerInfo</code> your success callback receives still describes the old plan. Code that reads the new product id out of the callback and writes it somewhere will write the old one.</p>
<p>Under <code>DEFERRED</code> the product id is also what matches the purchase callback to the transaction Google Play returns, which is another reason to pass the plain subscription id. Treat a deferred change as scheduled rather than done, and let the entitlement's <code>expirationDate</code> and your webhook pipeline tell you when it happened.</p>
<h2><strong>Other </strong><code><strong>PurchaseParams</strong></code><strong> options</strong></h2>
<h3><code><strong>isPersonalizedPrice</strong></code></h3>
<pre><code>val params = PurchaseParams.Builder(activity, packageToPurchase)
    .isPersonalizedPrice(true)
    .build()</code></pre>
<p>This is a compliance flag, not a pricing feature. It tells Google Play to disclose in the purchase sheet that the price shown was personalized for this user, which EU consumer law requires when that is true. The default is false, and the Amazon Appstore ignores it. Set it only when you are actually personalizing the price.</p>
<h3><strong>Add-ons</strong></h3>
<p>For Play Store subscriptions with add-ons, the builder takes extra items:</p>
<pre><code>@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
val params = PurchaseParams.Builder(activity, basePackage)
    .addOnPackages(listOf(addOnPackage))
    .build()</code></pre>
<p>There are matching <code>addOnStoreProducts</code> and <code>addOnSubscriptionOptions</code> overloads. All three sit behind <code>@ExperimentalPreviewRevenueCatPurchasesAPI</code>, and the restrictions from the KDoc are strict: Play Store subscriptions only, every add-on's renewal period must match the base product's period, and at most 49 add-ons per purchase.</p>
<h2><strong>Keeping RevenueCat's paywall UI</strong></h2>
<p>Now the case where RevenueCat draws the paywall. If the paywall you designed in the dashboard is what you want to render but you need your own code in the flow, you do not have to give up the UI.</p>
<p>The composable takes a single options object, and <code>dismissRequest</code> is a constructor parameter of the builder rather than a setter:</p>
<pre><code>Paywall(
    PaywallOptions.Builder(dismissRequest = { navController.popBackStack() })
        .setOffering(offering)
        .setListener(paywallListener)
        .setShouldDisplayDismissButton(true)
        .build(),
)</code></pre>
<p><code>setOffering(null)</code> falls back to the current offering. <code>setShouldDisplayDismissButton</code> defaults to <code>false</code> here, while <code>PaywallDialogOptions</code> and the activity launch options default it to <code>true</code>, so an embedded paywall with no close button is the expected behavior rather than a bug.</p>
<h3><code><strong>PaywallListener</strong></code><strong>: Observing the flow</strong></h3>
<p><code>PaywallListener</code> is an interface where every method has a default implementation, so you override only what you need:</p>
<pre><code>private val paywallListener = object : PaywallListener {
    override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {
        analytics.track(&quot;purchase_completed&quot;, storeTransaction.productIds)
        unlockContent(customerInfo)
    }

    override fun onPurchaseError(error: PurchasesError) {
        analytics.track(&quot;purchase_error&quot;, error.code.name)
    }

    override fun onPurchaseCancelled() {
        analytics.track(&quot;purchase_cancelled&quot;)
    }

    override fun onRestoreCompleted(customerInfo: CustomerInfo) {
        refreshUi(customerInfo)
    }
}</code></pre>
<p>The full set is <code>onPurchaseStarted</code>, <code>onPurchaseCompleted</code>, <code>onPurchaseError</code>, <code>onPurchaseCancelled</code>, <code>onRestoreStarted</code>, <code>onRestoreCompleted</code>, <code>onRestoreError</code>, plus <code>onUrlOpened</code> for links inside the paywall and <code>onWebCheckoutOpened</code> for the case where the user left to pay on the web. That last one is distinct from cancellation and needs handling separately, because the user has not given up, they have gone elsewhere to finish. If you also plan to supply purchase logic, read the last section first: four of these callbacks go quiet in that mode.</p>
<h3><strong>Gating the flow before it starts</strong></h3>
<p>The two hooks people want when they ask about customizing the purchase flow are not in the list above. They are the initiated callbacks, and they hold the flow open until you answer.</p>
<p>Each one receives a <code>Resumable</code>, a <code>fun interface</code> with <code>resume(shouldResume: Boolean)</code> and an <code>invoke</code> operator defaulting to true. So <code>resume()</code> proceeds and <code>resume(false)</code> aborts. If you never call it, the purchase never starts. Watch the import: the one <code>PaywallListener</code> wants is <code>com.revenuecat.purchases.ui.revenuecatui.utils.Resumable</code>. There is an identical <code>com.revenuecat.purchases.customercenter.Resumable</code> for Customer Center, and picking it gives you a confusing message about overriding nothing.</p>
<p>Both hooks are members of the same listener shown above:</p>
<pre><code>override fun onPurchasePackageInitiated(rcPackage: Package, resume: Resumable) {
    if (userIsSignedIn()) {
        resume()
    } else {
        showSignInSheet(
            onSuccess = { resume() },
            onDismiss = { resume(false) },
        )
    }
}</code></pre>
<p><code>onRestoreInitiated(resume)</code> gives you the same gate for restore. This is the place to require sign in, ask for a confirmation, or check an age gate, because it runs before Google Play's sheet appears rather than after the user has paid.</p>
<h3><strong>Getting a result from a full screen paywall</strong></h3>
<p>For a paywall presented as its own activity, results come back through <code>PaywallResultHandler</code>. Construct the launcher in <code>onCreate</code>. There is no <code>remember</code> based helper:</p>
<pre><code>class MainActivity : ComponentActivity(), PaywallResultHandler {

    private lateinit var launcher: PaywallActivityLauncher

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launcher = PaywallActivityLauncher(this, this)
    }

    override fun onActivityResult(result: PaywallResult) {
        when (result) {
            is PaywallResult.Purchased -&gt; unlockContent(result.customerInfo)
            is PaywallResult.Restored -&gt; refreshUi(result.customerInfo)
            is PaywallResult.Error -&gt; showError(result.error)
            PaywallResult.Cancelled -&gt; Unit
        }
    }
}</code></pre>
<p><code>PaywallResult</code> is a sealed class, so that <code>when</code> is exhaustive and a new case would break the build rather than fall through silently. A user cancellation arrives as <code>Cancelled</code> rather than as an <code>Error</code> carrying <code>PurchaseCancelledError</code>, because the activity maps that code before returning.</p>
<p>To launch it with a listener or custom purchase logic attached, use the options form:</p>
<pre><code>@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
fun showPaywall(offering: Offering?) {
    launcher.launchWithOptions(
        PaywallActivityLaunchOptions.Builder()
            .setOffering(offering)
            .setListener(paywallListener)
            .build(),
    )
}</code></pre>
<p>The opt in is required only because of <code>setListener</code> and <code>setPurchaseLogic</code> on this particular builder. <code>Paywall()</code>, <code>PaywallOptions</code>, <code>PaywallListener</code>, and <code>PaywallPurchaseLogic</code> themselves need none. There is also <code>launchIfNeededWithOptions</code>, which takes a required entitlement identifier or a <code>shouldDisplayBlock</code> and skips presentation when the user already has access.</p>
<p>One caveat shows up only in the field. The SDK passes the listener and purchase logic to the activity through an in memory store rather than through the intent, because neither is serializable. If the process dies while the paywall is open, the SDK logs that they were lost, finishes the activity, and returns <code>PaywallResult.Cancelled</code>. Do not treat that result as a signal that the user declined. This applies only when you attached one of them. A paywall launched without either carries no in memory key and recreates normally.</p>
<h3><strong>Custom variables</strong></h3>
<p>A dashboard paywall can contain <code>{{ custom.player_name }}</code> placeholders, and you supply the values:</p>
<pre><code>PaywallOptions.Builder(dismissRequest = { dismiss() })
    .setCustomVariables(
        mapOf(
            &quot;player_name&quot; to CustomVariableValue.String(&quot;John&quot;),
            &quot;level&quot; to CustomVariableValue.Number(42),
            &quot;is_premium&quot; to CustomVariableValue.Boolean(true),
        ),
    )
    .build()</code></pre>
<p>Both <code>{{ custom.key }}</code> and <code>{{ $custom.key }}</code> resolve. Resolution order is the value you passed, then the dashboard default, then an empty string with a warning logged. Keys must start with a letter and contain only letters, digits, and underscores, and the SDK drops invalid keys with a warning rather than surfacing an error, so check logcat if a placeholder renders empty. One exception: <code>PaywallDialogOptions.Builder.setCustomVariables</code> skips the validator, so a bad key there is never logged and simply resolves to empty.</p>
<h2><strong>Owning the transaction with </strong><code><strong>PaywallPurchaseLogic</strong></code></h2>
<p>This is the case where your own code completes the Google Play transaction and RevenueCat supplies only the paywall and the backend. You configure <code>PurchasesAreCompletedBy.MY_APP</code> and hand the paywall your purchase code.</p>
<p>The interface has two suspend methods. The purchase side receives the activity and a params object:</p>
<pre><code>class MyPurchaseLogic(
    private val billing: MyBillingClient,
) : PaywallPurchaseLogic {

    override suspend fun performPurchase(
        activity: Activity,
        params: PaywallPurchaseLogicParams,
    ): PurchaseLogicResult {
        return when (val outcome = billing.purchase(activity, params.rcPackage)) {
            is Outcome.Success -&gt; PurchaseLogicResult.Success
            is Outcome.Cancelled -&gt; PurchaseLogicResult.Cancellation
            is Outcome.Failed -&gt; PurchaseLogicResult.Error(outcome.error)
        }
    }</code></pre>
<p>Restore in this mode means telling RevenueCat what Google Play already knows, which syncPurchases does, so the implementation is usually thin:</p>
<pre><code>    override suspend fun performRestore(customerInfo: CustomerInfo): PurchaseLogicResult {
        return try {
            Purchases.sharedInstance.awaitSyncPurchases()
            PurchaseLogicResult.Success
        } catch (e: PurchasesException) {
            PurchaseLogicResult.Error(e.error)
        }
    }
}</code></pre>
<p>If your billing code is callback based, extend <code>PaywallPurchaseLogicWithCallback</code> and implement <code>performPurchaseWithCompletion</code> and <code>performRestoreWithCompletion</code> instead. The base class bridges them for you.</p>
<p>The result type has three cases and the exact names are easy to miss. It is <code>PurchaseLogicResult.Cancellation</code>, not <code>Cancelled</code>, and the error case carries a nullable <code>errorDetails</code>:</p>
<ul>
<li><code><strong>Success</strong></code> triggers <code>syncPurchases</code>. From <code>performPurchase</code> it also dismisses the paywall. From <code>performRestore</code> it dismisses only if a <code>shouldDisplayBlock</code> was set and no longer matches.</li>
<li><code><strong>Cancellation</strong></code> tracks a cancel event and leaves the paywall open on the purchase path. On the restore path it is silently ignored.</li>
<li><code><strong>Error(errorDetails)</strong></code> shows an error dialog only if you provide the error. <code>PurchaseLogicResult.Error()</code> with no argument does nothing at all: no dialog, no tracking. Always pass the error.</li>
</ul>
<p><code>params</code> is a <code>PaywallPurchaseLogicParams</code>, which is what makes this interface usable for plan changes. It carries <code>rcPackage</code>, <code>subscriptionOption</code>, <code>oldProductId</code>, and <code>replacementMode</code>, so a product change configured on the dashboard paywall reaches your code with enough information to reproduce it. One type detail will bite you on the next line: <code>replacementMode</code> is typed <code>ReplacementMode?</code>, the interface that both <code>StoreReplacementMode</code> and the deprecated <code>GoogleReplacementMode</code> implement. There is no public converter, so narrow it yourself with <code>params.replacementMode as? StoreReplacementMode</code> before passing it to a <code>PurchaseParams.Builder</code>.</p>
<p>The older <code>PurchaseLogic</code> and <code>PurchaseLogicWithCallback</code> types are deprecated in favor of these, because their <code>performPurchase</code> took only a <code>Package</code> and had nowhere to put the product change details.</p>
<p>Three behaviors of this mode need internalizing before you ship it:</p>
<ol>
<li><strong>The SDK no longer acknowledges purchases</strong>, so your code must. The KDoc on <code>PurchasesAreCompletedBy.MY_APP</code> states the consequence: failing to acknowledge within 3 days leads Google Play to automatically refund the user.</li>
<li><code><strong>MY_APP</strong></code><strong> with a null purchase logic does not fall back to normal behavior.</strong> <code>validateState</code> puts the paywall into an error state that renders instead of your paywall.</li>
<li><strong>Four listener callbacks stop firing.</strong> <code>onPurchaseStarted</code>, <code>onPurchaseCompleted</code>, <code>onRestoreStarted</code>, and <code>onRestoreCompleted</code> live only on the <code>REVENUECAT</code> branch. The initiated hooks still run because they execute before the branch, and <code>onPurchaseError</code>, <code>onPurchaseCancelled</code>, and <code>onRestoreError</code> can still fire because they come from a <code>catch</code> that wraps both branches. Anything you were doing in <code>onPurchaseCompleted</code> has to move into your <code>performPurchase</code> success path.</li>
</ol>
<h2><strong>Choosing an approach</strong></h2>
<ul>
<li><strong>Full design control, standard purchase handling</strong>: <code>PurchaseParams</code> with <code>purchaseWith</code> or <code>awaitPurchase</code>.</li>
<li><strong>Dashboard paywall embedded in your layout</strong>: <code>Paywall</code> with <code>PaywallListener</code>.</li>
<li><strong>Dashboard paywall as its own screen, returning a result</strong>: <code>PaywallActivityLauncher</code> with <code>PaywallResult</code>.</li>
<li><strong>Dashboard paywall, your own billing code</strong>: <code>PurchasesAreCompletedBy.MY_APP</code> with <code>PaywallPurchaseLogic</code>.</li>
</ul>
<p>The naming differences between these surfaces are not consistent, and the compiler will not always help. <code>PaywallOptions.Builder</code> has <code>setPurchaseLogic</code>, while <code>PaywallDialogOptions.Builder</code> calls the same thing <code>setCustomPurchaseLogic</code>. <code>PaywallResult</code> uses <code>Cancelled</code> while <code>PurchaseLogicResult</code> uses <code>Cancellation</code>. When something does not resolve, check which options class you are holding.</p>
<h2><strong>Conclusion</strong></h2>
<p>The habit worth taking from all of this is to treat the silent defaults as decisions you are making. <code>WITHOUT_PRORATION</code> is what a product change does when you say nothing about proration, and it is the mode that charges an upgrading user nothing today. <code>defaultOption</code> is what gets purchased when you pass a <code>Package</code>, chosen by a documented rule you can predict. Both are reasonable, and both are wrong for some paywalls, so pick them on purpose and write the reason next to the builder call.</p>
<p>The other shift is to stop reading a declined purchase as a failed one. The SDK separates a failure from a decision at every layer, from the boolean on the error callback to the result types the paywall returns to the hooks that let you abort before Google Play ever appears. Your error handling should draw the same line, because the user who backed out of the sheet is still a prospect and does not need to be told that something went wrong.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[BoldVoice took the X off its paywall, and refunds wiped out the extra trial starts]]></title>
      <link>https://www.revenuecat.com/blog/growth/anada-lakra-boldvoice-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/anada-lakra-boldvoice-sub-club-podcast-2026</guid>
      <pubDate>Thu, 17 Sep 2026 08:37:11 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Anada Lakra runs BoldVoice at $10M ARR with 10 people, every one of whom, engineers included, sits in two user interviews a week.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/b544ce78a91acc6523f5c86dfafcb4522b5f244d-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>BoldVoice tested two changes that every paywall optimizer has been tempted by. The first was a hard paywall: remove the dismiss button so nobody can proceed without starting a trial. The second was a softer trap: leave the X on the main paywall, but if a user dismisses it, show a percentage-off offer they can't close.</p>
<p><a href="https://www.youtube.com/watch?v=-5zgc14t2gc">Watch on YouTube</a></p>
<p>Both looked like wins. &quot;Both of those look so promising on the trial numbers,&quot; says Anada Lakra, BoldVoice's co-founder and CEO. &quot;And it makes sense, right? People have already put in all the time in the onboarding flow. They actually want to see what this app is all about.&quot; The team's instinct was to ship both to everyone.</p>
<p>Then the refunds arrived. &quot;The refund spiked by way more than the gain in start trials,&quot; Anada says. People who feel forced into a trial ask for their money back the moment the charge hits their bank account — and they leave with a grudge. &quot;Anything that kind of forces or tricks people into starting a trial, you'll pay it when it comes to refunds.&quot;</p>
<p>The fix was a different scoreboard. BoldVoice now judges paywall tests on net revenue after refunds, per exposed user, on a matured cohort — which means waiting at least an extra week for trials to convert and two more for refunds to settle before declaring anything. It's slower, and Anada admits the hardest part isn't discipline but patience: sitting on exciting data for a month or more. Asked at the end of the episode for her worst change of the past year, she doesn't hesitate: &quot;The paywall stuff. Just stop over-optimizing every single word and every button and everything. Optimize them for a little bit and then move on to the actual product.&quot;</p>
<h2><strong>Every employee does two user interviews a week — including the engineers</strong></h2>
<p>BoldVoice's user research isn't a function. It's a calendar rule.</p>
<p>Automated invites go out to a deliberate mix of users — subscribers of two or three years, people who started a trial last week and converted, people who didn't — asking for a 15-minute call. The invite automatically books whichever team members have space in their calendars. &quot;Every single person on the team — myself, my co-founder, our engineers, our growth people — everyone is in at least two user interviews a week,&quot; Anada says.</p>
<p>Two more layers sit on top. A super-user group meets roughly weekly, and engineers demo unshipped features to them, so heavy users shape the product before release. Users also come into the office so the team can watch what they tap and where they linger. &quot;Being able to actually verbally talk to users and actually see them interact with the product, I think is an underrated skill.&quot;</p>
<p>Anada is candid about the bias in this setup. Happy users book the calls. &quot;People who did not have a good experience are going to be less likely to jump on those calls.&quot; For them, BoldVoice uses a three-question survey answerable by replying to an email, and a short flow at cancellation asking what went wrong. Without that correction, she says, you end up making happy people happier and building a product that gets narrower and more technical instead of wider.</p>
<h2><strong>Why BoldVoice charged from day one — and won't copy Duolingo</strong></h2>
<p>BoldVoice launched in the first weeks of Y Combinator's summer 2021 batch and charged from the start: $10 a month, one-week free trial, no experimentation. That was the point. &quot;Charging for the product is a way to already figure out who that ICP is,&quot; Anada says. Willingness to pay correlates with the problem resonating, and it kept the team from chasing feedback from people who would never be customers. The energy went into onboarding and the core product instead of the paywall.</p>
<p>Pricing has since matured — BoldVoice leads with an annual plan at around $150 a year, offers monthly as a fallback, and has a $200-a-year tier with extra features like chat — but the philosophy hasn't moved toward freemium, and Anada is precise about why. &quot;The worst thing that someone can do is take some playbooks and some elements of a playbook from one app, combine it with a playbook of a different app, and create this mishmash strategy.&quot;</p>
<p>Duolingo's freemium base works because it teaches the basics of every language to a huge audience of hobby learners, and monetizing a fraction is enough. BoldVoice intentionally ignores that audience. Its users are intermediate-to-advanced professionals for whom the alternative is expensive one-on-one coaching with an accent coach: &quot;totally different ICP, different value proposition, different willingness to pay.&quot; The same logic ruled out the cliché college-campus launch, which BoldVoice tried. Students had less money, less urgency, and hadn't yet sat in the meeting where a colleague got listened to instead of them.</p>
<p>What BoldVoice has instead is a narrow free taste: dismiss the paywall and you get the speech assessment, the first daily plan, and a coach video, then a seven-day trial to continue on day two. David Barnard calls it a reverse trial. Anada calls it a middle ground that doesn't cost you the onboarding paywall for the people who are already ready to pay.</p>
<h2><strong>One Threads post, 100,000 people in Korea</strong></h2>
<p>BoldVoice's in-house ML team, working with the Hollywood accent coaches who help label its training data, built a tool that guesses where you're from after about ten seconds of speech. They shipped it as an ungated web app — no download, no trial — called the Accent Oracle, and Anada mentioned it in her monthly investor update.</p>
<p>&quot;One of them was an angel investor. It was Korean, and he was like, 'Oh, this is cool. I'll put it on Threads,'&quot; she says. &quot;And then I believe the next day we had 100,000 people in Korea try this thing.&quot;</p>
<p>The lesson she draws isn't &quot;build a viral tool.&quot; It's that you can't engineer virality, but you can add levers to something that already has it: a city-level guess re-spiked the Oracle, and showing users a preview of the fuller results waiting in the app lifted the number who made the jump. A TikTok trend of users crying at low scores gave the brand another unpaid spike — hundreds of millions of views for one creator, by her estimate — with the caveat that humor brings reach, not intent.</p>
<h2><strong>$10M ARR, 10 people, one meeting a week</strong></h2>
<p>Anada’s explanation for roughly $1M of ARR per employee is unglamorous. Hire founder-type doers who don't need direction. Hold one company-wide meeting, on Mondays, and leave the rest of the week for building and talking to users. Default users to the annual plan so paid acquisition pays back within the trial window and every channel's return is legible the same week.</p>
<p>And let everyone ship. &quot;Everyone in the company is using Claude. For the first time in my life, I'm actually shipping PRs myself, which is crazy. I'm not a technical person.&quot; The designer takes Figma work through to prototypes that are about 80% of the way to production before engineers touch them.</p>
<p>The part that keeps the small team defensible is the part that took longest to build. &quot;Still to this day, you cannot talk to ChatGPT or Claude or whatever have you and get this kind of feedback that BoldVoice gives you,&quot; Anada says — a deliberate bet that non-native English pronunciation is too narrow a niche for general models to chase and far too large a market to ignore.</p>
<p>In <a href="https://www.youtube.com/watch?v=-5zgc14t2gc" target="_blank" rel="noopener noreferrer">the full episode</a>, Anada also explains why BoldVoice is expanding from accent into the &quot;penultimate mile&quot; of spoken fluency, why she considers &quot;accent&quot; an almost anti-viral word, and what a self-imposed six-month term-sheet deadline did for her focus.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://www.linkedin.com/in/anadalakra/">Anada Lakra on LinkedIn</a></li>
<li><a href="https://www.linkedin.com/company/boldvoice">BoldVoice on LinkedIn</a></li>
<li><a href="https://www.boldvoice.com/">BoldVoice</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Why would someone pay for your app when AI does it for free?]]></title>
      <link>https://www.revenuecat.com/blog/growth/differentiator-against-ai</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/differentiator-against-ai</guid>
      <pubDate>Thu, 17 Sep 2026 08:12:40 GMT</pubDate>
      <dc:creator><![CDATA[Daphne Tideman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[6 ways to win against LLM chats and the AI apps rushing to copy you]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/9fcc76852ccd574cf31320a02399424ed283ddc5-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>I’m not going to tell you that AI is not coming for you. I know founders wake up at 2am thinking: “<em>Why would anyone pay for my app when AI does it for free?”</em></p>
<p>But I also don’t believe apps will disappear altogether. <a href="https://www.linkedin.com/in/ethan-garr/">Ethan Garr</a>, a growth advisor, shared on a <a href="https://www.youtube.com/watch?v=zvibfgjmWfs">Sub Club Live about building with AI</a> that a friend had said apps would be gone within six months. Not what you want to hear when you work in the app space. Yet here we are more than six months later: apps are still around, and more are being launched than ever before (<a href="https://www.revenuecat.com/state-of-subscription-apps/">7x more since 2022, to be specific</a>). So I don’t believe they’ll disappear. Yes, LLM chats are replacing certain apps. Yes, the space is changing faster than ever. But the data suggests there is still hope, and I’m willing to cling to that data.</p>
<p>Especially as I’ve found you can win the game if you focus on the areas where an LLM chat cannot beat your app.</p>
<p><em>&quot;...since March we’ve seen a significant spike in student interest in ChatGPT. We now believe it’s having an impact on our new customer growth rate.&quot;</em></p>
<p>When CEO Dan Rosensweig of Chegg, a homework-help subscription service, <a href="http://fortune.com/2023/05/02/chegg-shares-tumble-students-fleeing-chatgpt-a-i/">said this during an earnings call in 2023</a>, the impact hit hard:</p>
<ul>
<li>The company’s stock fell 50%</li>
<li>By 2025, <a href="http://cnbc.com/2025/10/27/chegg-slashes-45percent-of-workforce-blames-new-realities-of-ai.html">45% of their workforce were made redundant</a></li>
<li>By Q2 2026, the <a href="http://businesswire.com/news/home/20260806916599/en/">freefall continued</a> and they attempted to pivot away from homework toward an employability platform</li>
</ul>
<p>Every step of the way, the blame was the same: AI.</p>
<p>Now take Duolingo: ChatGPT, and most LLMs for that matter, can also tutor you, for free. Google has layered live translation into its products — only a few months ago, I did a full strategy session with a subscription app startup in Portuguese (even though I don’t speak a word of Portuguese). The experience was honestly mind-blowing. I’m curious what I’m like in Portuguese.</p>
<p>Yet Duolingo isn’t floundering:</p>
<ul>
<li><a href="http://investors.duolingo.com/static-files/3c8277ee-bc94-4f5d-9b77-0db3e46f88b8">Q2 2026 revenue was up 18%</a></li>
<li>DAU rose 23% to 58.7 million</li>
<li>Paid subscribers increased 17% to 12.7 million</li>
</ul>
<p>By contrast, Duolingo’s CEO Luis von Ahn <a href="https://www.google.com/url?q=https://fortune.com/2025/08/27/duolingo-existential-crisis-ai-google-translate-language-learning-live-translation/&amp;sa=D&amp;source=docs&amp;ust=1789606406843987&amp;usg=AOvVaw1jQnNb8TpuRNtpWbeqmhYA">seems largely unimpressed by AI competitors</a>:</p>
<p>“Just having conversations in French on something like ChatGPT gets pretty boring after a while. It doesn’t keep you there. We keep you on task with all the gamification.”</p>
<p>So what explains the difference between the two? Why is one being, frankly, obliterated by AI, while the other’s owl is gleefully celebrating its success?</p>
<p>It’s about the app itself: while both companies create educational content, Chegg’s product is essentially a one-shot answer: a lookup experience without enough built around it in terms of data, habit, and workflow. <a href="https://www.revenuecat.com/blog/growth/cem-kansu-duolingo-sub-club-podcast-2026">Duolingo</a>, meanwhile, has spent 14 years building gamification systems and habit-forming product design that are much harder to replicate.</p>
<h2>People will pay for AI apps, they just won’t stay</h2>
<p>One thing worth separating before we dive into the numbers is what we mean by an ‘AI app’, because we often talk about these all under AI:</p>
<ul>
<li>An LLM chat is ChatGPT, Claude, Gemini</li>
<li>An AI app is a subscription app with AI as a central feature inside it, like Cal AI or Rosebud (and quite possibly yours)</li>
</ul>
<p>The first is the free competitor keeping you up at night. The second might be your own app. The data below is about the second.</p>
<p>AI-powered apps are casually out-monetizing and outperforming non-AI apps, according to RevenueCat’s <a href="https://www.revenuecat.com/blog/growth/subscription-app-trends-benchmarks-2026/">State of Subscription Apps 2026</a>:</p>
<ul>
<li>41% more revenue per payer</li>
<li>52% better trial conversion</li>
</ul>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ec6d4280ae252655b8ac7dafa36f307051cafe8b-1079x764.png" alt=""/></figure>
<p>But there is a flip side to this:</p>
<ul>
<li>12 month retention of 21.1% vs. 30.7%</li>
<li>Refunds are about 20% higher</li>
</ul>
<p>So <a href="https://www.revenuecat.com/blog/growth/ai-app-retention-study">consumers are willing to pay for AI apps, but the harder challenge is getting them to keep paying</a>. </p>
<p><a href="https://www.faceapp.com/">FaceApp</a> used AI to apply an age filter, allowing you to see an aged version of yourself (why anyone would want that is beyond me). It peaked with <a href="https://www.businessofapps.com/data/faceapp-statistics/">downloads growing by 2000%</a>, only to see a 75% drop the next month because of privacy concerns and the novelty wearing off. From 2019 to 2020, growth stagnated, with ARR remaining around $25 million (not a bad number to be stuck at, but a change in growth pace for sure).

FaceApp survived the challenges, but had to optimize the app to focus on a broader use case, from photorealistic beauty (e.g. applying makeup) to selfie retouching. Broadening their use case helped them better retain users — and in <a href="https://www.businessofapps.com/data/faceapp-statistics/">2025 they hit $150 ARR.</a></p>
<p>So the question in this article’s title is secretly the wrong one: people <em>will</em> pay. They’re already paying more — and faster — for AI-powered apps than for almost anything else right now.</p>
<p>The real question is <strong>why will they still be paying in month six?</strong> That’s a product question, not a marketing one, and it’s the question the rest of this article sets out to answer. (It’s also a much nicer question to wake up to at 2 am.)</p>
<p><strong>AI alone isn't enough to win customers permanently</strong>, and Sora proves it: when the company that builds the model can't keep people, the model was never what kept them. Which is also why I don't think the other AI apps or copycat AI apps in your category are your biggest competitors.</p>
<h2>ChatGPT is the biggest ‘window’ ever built</h2>
<p>A while ago, I was standing in the kitchen, checking several weather apps on my phone, trying to decide whether to walk the dog now. I turned around to find my father-in-law looking at me like I’d lost it: &quot;Why are you doing that? Just look out the window.&quot;</p>
<p>Weather apps’ competitors aren’t other apps; they are looking out the window, asking around, or even just going to stand outside.</p>
<p>In the world of apps, your window is ChatGPT, Claude, or whatever other LLM chat of your choice. The free models, for now, can do a lot, and we see growing usage in categories of apps that support them, e.g, <a href="https://www.pewresearch.org/internet/2026/06/17/americans-and-ai-2026-chatbots-smart-devices-and-views-on-impact/">one in five US chatbot users ask for medical advice, and as many again about diet and fitness</a>.</p>
<p>The second, more meta way it is your window competitor is the ability to help you and others build faster. App launches went from about 2,000 to 14,700+ per month in four years; the volume is huge.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/3b5aa03170ea86244228ae897fcce78ae15b09b3-1078x778.jpg" alt=""/></figure>
<p>But in that, we still see 69% of subscription revenue going to pre-2020 apps, versus just 3% to apps launched in 2025 and 2026. <a href="http://subclub.com/episode/why-app-economy-disruption-wont-happen-as-fast-as-everyone-thinks-eric-seufert">Eric Seufert argues that</a> while the cost of building is lower, the cost of distribution has increased because apps are all chasing the same attention. Rik Haandrikman echoes this: <a href="https://www.revenuecat.com/blog/engineering/building-apps-existing-audience">distribution is a moat.</a></p>
<p>Those who are holding on to that 69% know what a weather app can do that a window can’t, what their app can do that an LLM can’t. Those who can’t explain the differentiator of their app are the ones slowly dying under the weight of AI's growth.</p>
<h2>Anatomy of the apps AI has actually killed</h2>
<p>This is not a random murder spree; AI’s victims share something in common (I clearly need to stop reading Swedish noir novels). The apps replaced by LLM chats have one or more of these four things in common:</p>
<ol>
<li>No structure</li>
<li>No memory</li>
<li>No habit</li>
<li>No precision advantage</li>
</ol>
<p>Most apps being replaced are a single question or task and an answer. That was Chegg’s real problem: strip away the branding, and the product was a prompt with a subscription attached. <a href="https://stackoverflow.com/questions">Stack Overflow</a>, a question-and-answer platform for developers, is on the same path, with <a href="https://www.similarweb.com/blog/insights/ai-news/stack-overflow-chatgpt/">traffic falling around 6% every month since early 2022</a>. Why? An LLM gives you the same answer instantly, in a conversation, without posting a question and hoping a stranger replies.</p>
<p>Dan Layfield, Founder of Subscription Index, <a href="https://subclub.com/episode/the-subscription-growth-formula-churn-math-retention-wins-and-smart-product-bets-dan-layfield-subscription-index">made a point on Sub Club</a> that explains the pattern: <strong>your retention is dictated by how long the user has the problem you solve</strong>. Phone plans are retained for decades because the problem never goes away. An answer-lookup problem dies the second the answer arrives — and now the answer arrives in three seconds, for free. Chegg's real problem was never ChatGPT; it was that the problem they solved only lasted one homework question at a time.</p>
<p>The uncomfortable truth: <strong>if your app’s value fits in one prompt and one reply, you are in the blast radius.</strong></p>
<h2>The Blank Box Test (and six things a blank box cannot do)</h2>
<p>I have a challenge for you called the Blank Box Test. It’ll only take two minutes:</p>
<ol>
<li>Open your LLM chat of choice</li>
<li>Write out your user’s problem in their words</li>
<li>Analyze the results vs. what your app offers</li>
</ol>
<p>Hopefully, you’ll see one of six advantages appear that you can lean into further. If not, your app is replaceable. So consider each of these six areas and see which could help you effectively beat an LLM. With all of these, yes, there are ways an LLM could do these things, but a general chat doing all six — or even one of them — to a very high level for everyone’s daily needs? Unlikely.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/9a984b6f5cd6eab7f51a1ee60c8fc495b25fed98-2048x1091.jpg" alt=""/></figure>
<p>And to be clear, this isn't AI apps vs. non-AI apps. Cal AI, Rosebud, and Tolan are all examples that lean heavily on AI. The difference is that <em>AI isn't what they're selling you</em> — rather, it’s part of their infrastructure that helps them lean into one of the six differentiators against LLMs. That’s why I’ve also intentionally shared several non-AI examples.</p>
<h3>1. Structure</h3>
<p>I use <a href="https://www.deliciouslyella.com/membership">Deliciously Ella</a> and <a href="https://www.mob.co.uk/">Mob Kitchen</a> for recipe inspiration. Until I had what I thought was a genius idea: why not use Claude to build a weekly meal plan based on what I already have in the fridge, my dietary requirements, the foods I love, and the kinds of food that work well for me as someone with ADHD and an intense sweet tooth? A perfect, personalized plan.</p>
<p>So I did exactly that. I used it for a few weeks, but eventually went back to my usual apps because I missed the beautiful visuals of seeing what I was going to cook. I missed feeling inspired and getting excited about new recipes. The checklists and text in Claude just weren’t doing that.</p>
<p>While I probably could have forced Claude to create images and build everything into a visual weekly dashboard, the structure of the apps is what gives them their value. I can browse them on the go, discover recipes as they catch my eye, and build my shopping list along the way. It’s not worth it to me to rebuild that, and I’d argue that for most average consumers it wouldn’t be.</p>
<p>I also think that’s why apps like <a href="https://www.runna.com/">Runna</a> are still winning against LLMs. For some people, a quick plan generated by an LLM will be enough for their daily run. But Runna gives you the full calendar, guidance during your run, and the exact structure of every workout you can follow live. <strong>It turns a plan into an experience</strong>, and that’s where the added value lies.</p>
<h3>2. Memory</h3>
<p>Luckily, the days when our LLM chats acted like Dory from <em>Finding Nemo</em> are gone. Building structures like second brains (a persistent, semantically searchable digital knowledge with AI) can allow your LLM to have so much more memory than ever before. But you still have to get it to use that data in some cases to make your experience better, and not all data sources are easy for the average AI user to connect to an LLM.</p>
<p>Meanwhile, your app can use the accumulation of user data so that by month six it is so much better than it was in month one, in ways you may not have even thought of. Memory + judgment of the right prompts are powerful.</p>
<p>This is the investment part of the <a href="https://www.nirandfar.com/how-to-manufacture-desire/">Nir Eyal Habit Loop</a>: getting users to invest in your app. If you do so, ensure that the investment pays back, and you’ll be able to add value to their experience, like <a href="https://www.rosebud.app/">Rosebud</a>, an AI journaling app that collects your data over time and uses it to better reflect and learn with you.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/a85a410504734c832482c0b1e42d4a875761208b-2000x1125.webp" alt=""/></figure>
<h3>3. Habit</h3>
<p>Habit comes down to accountability: being able to push someone to take action, whether that’s through streaks, reminders, or other <a href="https://www.revenuecat.com/blog/growth/gamification-in-apps-complete-guide">forms of gamification</a>.</p>
<p>Duolingo is a perfect example. You can ask an LLM to help you build a habit out of something, and it can tell you the theory behind habituation. Having it embedded in your app in a beautiful, intuitive way, with all those reminders and nudges already built out — it’s technically possible with AI, but not something the average user is willing to do themselves.</p>
<p>The numbers behind those nudges are not small either. When Duolingo introduced leaderboards, <a href="https://www.lennysnewsletter.com/p/how-duolingo-reignited-user-growth">Jorge Mazal, their former CPO, shared</a> that overall learning time went up 17% and the number of highly engaged learners tripled.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/351f9bb3cd253e48f847a32371cc8053c661ae42-920x820.png" alt=""/></figure>
<p>Once a streak passes 10 days, the odds of someone quitting drop sharply. An LLM can explain habit theory to you beautifully — but it won’t cause a terrifying owl to chase you at 9pm because your streak is about to die.</p>
<h3>4. Precision</h3>
<p><a href="https://www.calai.app/">Cal AI</a> launched in the middle of the rise of LLM chats in May 2024. Within a year, its teenage founders were claiming around <a href="https://techcrunch.com/2026/03/02/myfitnesspal-has-acquired-cal-ai-the-viral-calorie-app-built-by-teens/">$2 million in monthly revenue, and by the time MyFitnessPal acquired it, it had passed 15 million downloads</a>. The acquisition also gave them MyFitnessPal's food database of 20 million foods and 68,500 brands, allowing them to redesign and improve the product.</p>
<p>You could argue that their product is just a simple question-and-answer tool, but the precision with which they can tell you what is in your food, thanks to all that data and insight, is much better than asking ChatGPT. Especially if you combine it with their structure for organizing and tracking your calories and intake. Gives us hope, right? If two teenagers can beat LLM chatbots, so can we.</p>
<p><a href="https://merlin.allaboutbirds.org/">Merlin</a>, a bird-sound app, is another great example of this. They have a curated acoustic library built from decades of <a href="https://ebird.org/home">eBird</a> and Macaulay Library data and have <a href="https://www.birds.cornell.edu/home/the-magic-of-merlin">10 million active users</a>. That dataset and the precision to recognize bird sounds effectively are their strengths.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/e00e768546945b919006f64a7df522e127e849cd-700x560.webp" alt=""/></figure>
<h3>5. Connection</h3>
<p>Trust, tone, and brand feel go a long way in building a connection. Look at <a href="https://www.tolans.com/">Tolan</a>, the AI companion. It builds a personality that stands out and gives a feeling of connection. <a href="https://www.revenuecat.com/blog/growth/ajay-mehta-sub-club-podcast-2025">Its founder argues</a> that doing this well, with voice, memory, and character, is exactly what LLM Chats are not set up to focus on.</p>
<p>But connection goes further than an app that knows you. It’s about belonging: Duolingo’s leagues, Strava’s clubs, the run crew that only exists because the app brought them together. People don’t cancel things that feel like a part of them. <a href="https://www.revenuecat.com/blog/growth/solve-app-problems-emotionally">Emotional connection is a strong driver of retention</a>. I also find it telling that <a href="https://techcrunch.com/2025/08/12/ai-companion-apps-on-track-to-pull-in-120m-in-2025/">AI companion apps are on track to pull in $120 million</a> even though chatting with an AI is free: people pay for a relationship, not a reply.</p>
<p>In a world flooded with vibe-coded apps, those that take the time to build connection and an experience stand out.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/3499d378643f190b8afee4e7522c60afa4b18a8a-528x587.jpg" alt=""/></figure>
<h3>6. Digital and physical</h3>
<p>I also believe there’s room for another advantage in subscription apps that combine physical and digital to deliver value. I use a subscription app called <a href="https://plantwithwillow.co.uk/">Willow</a> to better understand what my plants need and know when to water them, when to move them to a sunnier or less sunny spot, and more.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/239524ddcde2111866ec02a7470a239b2bcd0802-1024x682.png" alt=""/></figure>
<p>They can do this better than an LLM chat by itself because they have sensors to feel and gather this data. While we can connect LLMs to other data sources, I believe there’s room for the physical advantage of naturally creating combined experiences with reality to add extra value.</p>
<h3>The power of multiple moats</h3>
<p>The combination of a few of these has allowed even ‘wrapper’ apps to survive and thrive. Take <a href="https://www.costarastrology.com/">Co-Star</a>, an astrology app: readings are written by a mix of AI and human writers, and Midjourney thought its packaging and ritual were worth <a href="https://techcrunch.com/2026/07/24/midjourney-acquired-the-astrology-app-co-star/">acquiring in 2026</a>. Yet users pay anyway: for the packaging, the birth-chart personalization, and the daily push ritual. That is a combination of structure and habit helping them win.</p>
<h2>What this means for what you build, say, and charge</h2>
<p>Firstly, you need to understand which of these six differentiators are available to your category and deliberately invest in them, figuring out what can truly make you different from an LLM chat or a quickly-created AI competitor. It will help you speak to your most loyal users and understand: if they are using LLMs, <a href="https://www.revenuecat.com/blog/growth/product-market-fit-subscription-apps/">why are they still choosing you</a>? If 95% of your audience still sees that value and understands it, and it’s not just down to brand loyalty, that helps you work out which of these matters most.</p>
<p>With distribution being the challenge now, your existing audience is a moat in itself. Investing in <a href="https://www.revenuecat.com/blog/growth/premium-win-lifecycle-messaging">retaining users through the initial experience and onboarding</a>, by building in guidance and value, is going to help you win.</p>
<p>From there, communication comes down to marketing and sharing outcomes that a blank box can’t promise. You want to sell the job you helped them achieve, not the engine. I’m not the only one who cringes at the label ‘AI-powered’ — Irrational Labs conducted <a href="http://growthunhinged.com/p/ai-messaging-study">a study of 767 software users</a> in which AI-powered features actually <em>lowered</em> the perceived value and did nothing to justify the higher price. <strong>AI is a tool, not an outcome.</strong></p>
<p>Finally, don’t risk getting into a race to the bottom. Yes, competition is fierce, but <a href="http://revenuecat.com/blog/growth/ai-subscription-app-pricing">competing against the $0 competitor won’t help you win</a>. Price isn't a long-term differentiator, and we see that in the <a href="https://www.revenuecat.com/blog/growth/subscription-app-trends-benchmarks-2026">State of Subscription Apps report</a> too: high-priced cohorts hold at 6x the LTV of lower-priced cohorts, and premium positioning signals the differential you’ve built. You just need to make sure you deliver that value for that price. Your <a href="https://www.revenuecat.com/blog/growth/freemium-tier-design">free tier</a> is competing with that blank box too, since it is the more limited experience, so make sure your free experience shows off the same advantages.</p>
<h2>The irony of it all</h2>
<p>We’ve talked about LLMs like ChatGPT as competitors. We talked about them helping your competitors build, but there’s an additional challenge; they are also your distribution channel. You can’t completely ignore them or move away from them. They need to know your app in order to suggest it to users. And they are undeniably useful — <a href="http://lennysnewsletter.com/p/why-chatgpt-will-be-the-next-big-growth-channel-brian-balfour">Brian Balfour recommends integrating them early</a> in your app and if you can, letting their ecosystem help you win while you strategically differentiate.</p>
<p>Your users will continue to pay for you if your app knows them (memory), structures them (opinionated workflow), holds them to it (habit), gets the details right (precision), feels like somewhere they belong (connection), and helps them get more value out of the world around them (physical and digital). Run the two-minute Blank Box Test this week if you haven’t, and start building the things a blank box cannot do.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Rico Can Now Take Action]]></title>
      <link>https://www.revenuecat.com/blog/engineering/rico-can-now-take-action</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/rico-can-now-take-action</guid>
      <pubDate>Mon, 14 Sep 2026 23:01:57 GMT</pubDate>
      <dc:creator><![CDATA[Austin Blake]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Rico can now make the changes for you, not just suggest them. I asked it to build a paywall experiment off my default paywall, and it created the Offerings, the Packages, and a running test — no dashboard required.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/571b903d51db2352b9af89b81c719664e6601b73-1774x887.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>I told Rico to build a paywall experiment off my default paywall. It created the new Offerings, the Packages, and the second paywall. Even better, it checked with me at each step and handed me a running test. Best of all? I never opened the dashboard.</p>
<p>That's it. That’s the update. Rico used to answer questions about your app business and suggest what to do next. Now, it can just go get the work done.</p>
<p><a href="https://www.youtube.com/watch?v=C2xkxGBwHZo">Watch on YouTube</a></p>
<h2>Rico can change your setup for you</h2>
<p>With your approval, Rico can create and manage products, Offerings, Packages, and Entitlements, and build or edit paywalls directly in RevenueCat.</p>
<p>That includes real store products, not just RevenueCat-side config. Rico can create and update the subscriptions that live in App Store Connect and Google Play. These features rolled out to everyone last week, so go check out your account right now.</p>
<h2>Experiments are the part I'd try first</h2>
<p>Every dev I talk to says the same thing about experiments: they’re easy to come up with, but a pain to set up. You have to duplicate the paywall, name everything, wire up new Offerings and Packages, keep track of which variant is which, and only then can you finally hit start.</p>
<p><strong>Rico can do that whole chain now.</strong> Ask it to test a new price or a new paywall against your current one and it builds out everything the test needs. Rico can create, update, start, stop, pause, and resume experiments, so the follow-up work is also just asking.</p>
<h2>You still approve everything</h2>
<p>Every proposed change requires your approval before anything happens. Rico is limited by the access level of the collaborator using it, so it can't do anything you couldn't do yourself in the dashboard. Project admins can go further and limit what Rico is allowed to do across all collaborators from project settings.</p>
<p>You can limit Rico to three levels of access: Read &amp; write with permission (to enable everything we’ve talked about here), Read only (to use Rico only for gaining insights to your data, but not for changing anything), and Disabled (will completely turn off all Rico integrations for this project).</p>
<h2>It's on your phone</h2>
<p>RevenueCat Mobile 2.0 brought Rico to iOS alongside the Paywalls AI Editor. Ask questions about your app business, attach a screenshot, approve Rico's actions, or generate a new paywall, all from your phone.</p>
<p>What’s more, Siri and App Shortcuts can now pull up MRR, revenue, active subscriptions, and trials without opening the app at all.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/aaa64c52c30d1af74a89de6f7abe61b3f2a0ec02-1179x1175.png" alt="Using Siri Shortcuts to view data in RevenueCat."/></figure>
<p>The Android release with Rico is in testing now.</p>
<h2>Go ask Rico</h2>
<p>The <a href="https://www.revenuecat.com/docs/tools/rico">Rico docs</a> have the full list of what Rico can and can't touch — or of course, you can just ask Rico. Have fun with it! Use Rico to learn more about what’s working or not in your app monetization scheme, then ask it to fix it. I’m looking forward to hearing about what you do with Rico.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Filling the silence: the post-subscription mistake that quietly causes churn]]></title>
      <link>https://www.revenuecat.com/blog/growth/premium-win-lifecycle-messaging</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/premium-win-lifecycle-messaging</guid>
      <pubDate>Thu, 10 Sep 2026 13:00:07 GMT</pubDate>
      <dc:creator><![CDATA[Alice Muir Kocourková]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Getting new subscribers to their first premium win — before they start looking for the cancel button]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/44454700c5e67a5d77a032ea888ccc9f8fa858f5-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Here's something most subscription app teams don't want to hear: the moment a user converts is also the moment many of them start drifting toward cancellation.</p>
<p>According to the <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps 2026</a>, 84% of 3-day trial cancellations and 64% of 7-day trial <a href="https://www.revenuecat.com/blog/growth/post-purchase-screen">cancellations occur between Day 0 and Day 1</a>. What’s more, nearly half of monthly subscribers, and the majority of yearly subscribers, <strong>do not renew even once</strong>.</p>
<p>By the end of this article, you'll have a clear picture of why that early churn happens — and a practical framework for preventing it. We'll cover the onboarding gap that quietly costs apps revenue every month, why new subscribers need to be treated like new <em>users</em>, and how to design a simple lifecycle journey that gets paying customers to their first 'premium win' — before buyer's remorse has a chance to set in.</p>
<h2>Stop treating conversion as the finish line</h2>
<p>Most subscription apps treat conversion as the finish line. But really it's the start of the retention journey. For many new subscribers, it's also where things start to go wrong.</p>
<p>Think about how much effort goes into getting a user to subscribe. Teams spend weeks, sometimes months, designing onboarding flows, refining paywall timing, testing trial journeys, tweaking upgrade messaging. Then the user finally converts… and the silence is deafening.</p>
<p>For new users, the app suddenly going quiet is a drastic 180. They were being nurtured towards the subscription they just paid for — and suddenly nothing. For app teams, that silence feels logical: the user paid, they're clearly engaged. Why interrupt them?</p>
<p>But <strong>subscribing doesn't mean the user actually </strong><em><strong>understands</strong></em><strong> what they've just unlocked</strong>. They might have converted based on a single feature they glimpsed during a trial, a discount that felt too good to pass up, or just a vague sense that this app might be useful. What they haven't done (yet) is experience the product as a paying customer. And without a little guidance, many of them will cancel before they ever do.</p>
<h3>Why do users cancel early?</h3>
<p>Early cancellations are rarely about product quality. They're almost always about <strong>the gap between what a user expected</strong> when they signed up, and <strong>what they actually experienced</strong> in those first few days. <a href="https://www.revenuecat.com/blog/growth/hard-paywall-activation-journey">That gap is where buyer's remorse lives</a>. And silence from the product team is what lets it take root.</p>
<p>There's another force making this worse: <strong>subscription fatigue</strong>. People are drowning in recurring charges — music, fitness, productivity, entertainment — and many have started turning off auto-renew the moment they subscribe to something new, just to feel in control. </p>
<p>It's a defensive habit born from too many forgotten subscriptions quietly draining bank accounts. Which means your new subscriber may have already mentally scheduled their exit before they've even opened the app a second time. <a href="https://www.revenuecat.com/blog/growth/first-renewal-churn">The window to prove your product is worth keeping isn't weeks — it's days</a>.</p>
<h2>Re-onboard existing subscribers like they're new users</h2>
<p>This is what's sometimes called the post-subscription gap: the window between a user converting and a <a href="https://www.revenuecat.com/blog/growth/subscription-app-expand-value">user genuinely getting value from what they paid for</a>.</p>
<p>It tends to show up in three connected ways:</p>
<h3>1. Upgraders don’t find the premium features</h3>
<p>This sounds almost too simple to be a real problem, but it is. And if you’re working with a freemium model, it’s quite possible the silence after conversion is what’s quietly pushing subscribers away.</p>
<p>Users who have already gone through free onboarding learn how to use the <em>free</em> version of your app. When they upgrade, nobody shows them what's changed. So they keep using the product the same way they always did — as if they're still on the free tier. The premium features exist, but the subscriber never finds them, and eventually cancels feeling like they never got much value when they started paying.</p>
<h3>2. Most apps stop communicating once a user converts</h3>
<p>After all that effort to nurture a potential subscriber, silence isn't a neutral choice — it's a missed opportunity. <a href="https://subclub.com/episode/how-to-boost-retention-with-subscription-lifecycle-messaging-alice-muir-phiture">When lifecycle messaging disappears</a>, users lack the product enablement to benefit from their upgrade, and often forget they even subscribed. They may question whether it was worth it, and cancel when renewal rolls around because they can't quite remember why they signed up.</p>
<h3>3. “They’ve already been onboarded” is the wrong mental model</h3>
<p>The mental model many teams use (&quot;they've already been through onboarding, they don't need more&quot;) is a big mistake. It also misses the nuance between different types of subscriber: an experienced free user who upgrades doesn’t need the same input as instant or zero-touch buyers. But that doesn’t mean they don’t need any attention — they’ve just entered into a completely different relationship with your product. They deserve to be welcomed and shown around, just like new subscribers; simply in a different way.</p>
<h2>What premium re-onboarding actually looks like</h2>
<p>Re-onboarding for new subscribers isn't about repeating everything from free onboarding. It's about specifically welcoming people into the premium experience — making it immediately obvious what they've unlocked and how to start using it.</p>
<p>Slack does this well. When a user starts a Pro trial, they immediately receive an email that doesn't just say 'your trial has started' — it tells them exactly what's changed. The headline feature gets its own moment ('Message &amp; file history unlocked'), followed by a clear list of everything else now available: Slack Connect channels, group huddles, unlimited canvases. There's no ambiguity about what the upgrade means. And crucially, the call to action isn't 'explore your account' — it's 'Explore in Slack', pointing users straight back into the product.</p>
<p>That's the whole job:</p>
<ol>
<li>Confirm the decision</li>
<li>Show what's unlocked</li>
<li>Direct to first action</li>
</ol>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/820dca534ee05b0a32181efee6b35d20d66dd4d5-677x1594.png" alt=""/></figure>
<p>Monday.com does something similar. When a user starts a trial, they immediately receive a message encouraging them to dive back in and create their first workspace. They also point to next steps for further guidance, like tutorial videos, blog content and access to support.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/058916b6b857db647532524cabc51657777ad481-680x1243.png" alt=""/></figure>
<p>The job of post-subscription messaging is to answer three questions for your new subscriber:</p>
<ol>
<li><strong>Where are the premium features?</strong> Show them, don't assume they'll find them.</li>
<li><strong>How do I actually use them?</strong> A list of features <a href="https://subclub.com/episode/the-art-of-driving-retention-through-product-ben-gammon-ladder">is not the same as a guide to a first action</a>.</li>
<li><strong>Did I make the right call?</strong> This is the question every new subscriber is quietly asking. Your job is to reassure them the answer is yes (and quickly).</li>
</ol>
<p><strong>The goal is to move someone from &quot;I paid for this&quot; to &quot;I just did something I couldn't do before&quot; as fast as possible. </strong>That first premium success — sometimes called a ‘premium win’ — is the moment a subscription stops feeling like an expense, and starts feeling like a habit.</p>
<h2>Define your premium win and build toward it</h2>
<p>Every product has its own version of a premium win, and figuring out what yours is should be one of the first things your retention team works on.</p>
<ul>
<li><strong>Strava</strong>’s premium win is seeing your performance data and training insights — the kind of analysis that makes a serious runner feel like they have a coach in their pocket</li>
<li><strong>Calm</strong>’s premium win is completing a sleep story or unlocking a full meditation course after a few free sessions have already built the habit</li>
<li><strong>Quizlet</strong>’s premium win is the moment a student gets access to Test Mode after experiencing the study tools for free</li>
</ul>
<p>What all of these have in common is that they're concrete, experiential moments — not a feature list, not a marketing promise, but an actual thing the user did that they couldn't do before. They’re <a href="https://www.revenuecat.com/blog/growth/hard-paywall-activation-journey">proof of the meaningful value your app promised</a>.</p>
<p>A simple way to think about your post-subscription messaging is to design it like a seven-day journey, but for people who just converted:</p>
<ul>
<li>Day zero is a warm welcome that tells them exactly what to do first</li>
<li>Day one or two is a nudge toward that specific premium win</li>
<li>Day three is a check-in where you celebrate what they've done and point them to what's next</li>
<li>By day five or six, you're spotlighting a feature based on what they've actually engaged with, not a generic list</li>
</ul>
<p>Duolingo does a version of this brilliantly with their weekly progress reports, which don't just remind users to practice — they show users what they've already accomplished, making the subscription feel like proof of progress rather than just a recurring charge.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/0e6889621f7b86ed4b8eee07b2defca7f825d083-1305x900.png" alt=""/></figure>
<h2>Four examples of lifecycle messaging to keep value visible long-term</h2>
<p>The post-subscription gap is most dangerous in the first week or two. But the underlying problem — subscribers gradually forgetting why they're paying — doesn't go away after that. It just becomes more hidden.</p>
<p>This is where longer-term <a href="https://www.revenuecat.com/blog/growth/lifecycle-marketing-campaigns-optimize-revenue">lifecycle messaging</a> earns its keep.</p>
<p>Think of it as <strong>resurfacing value</strong>:</p>
<ul>
<li>Helping users connect to real outcomes rather than abstract features</li>
<li>Reflecting their progress back to them</li>
<li>Reminding them why they started</li>
</ul>
<h3>Lifesum: sharing feature updates</h3>
<p>Lifesum sends messages to users when they improve a core feature — not just to announce the update, but to give subscribers a reason to re-engage with something they might have been using on autopilot.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b83aac2ea011ab4645abab729a82e64306ef8e89-355x897.png" alt=""/></figure>
<h3>Netflix: personalization and recommendation</h3>
<p>Streaming apps like Netflix and HBO Max take a <a href="https://www.revenuecat.com/blog/engineering/the-ultimate-guide-to-subscriber-segmentation-for-apps">personalized approach</a>, sending recommendations for what to watch next based on subscribers’ viewing history. It's a simple idea, but it solves the friction point of decision fatigue. When the next step is obvious and feels relevant, users take it. When they have to think about what to do, they often don't bother — and habit breaks are exactly where churn starts.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/9f115bee69aef9d2516f436103662ca598a4f114-1408x901.png" alt=""/></figure>
<h3>Dropbox: feature spotlight</h3>
<p>Feature spotlights work particularly well for subscribers who haven't explored everything they've paid for. A targeted message that says &quot;hey, you've never tried X, and based on how you use the app, it might be your new favorite thing&quot; is infinitely more useful than a generic &quot;here's everything we offer&quot;. It's also a retention intervention — that untouched feature might just be what would have justified the subscription when they hit renewal.</p>
<p>One example is Dropbox, who reach out to people using Dropbox to save and share images, inviting them to try their new image search feature. The email quickly conveys why it’s relevant for the user, what the benefits of the undiscovered feature are, and where to go next.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/a865ded7df87a47e083d7ade12d423af1ef72db0-680x1472.png" alt=""/></figure>
<h3>Strava: user identity reinforcement</h3>
<p>Finally, don't underestimate the power of identity reinforcement. Strava tells users when they earn a ‘Local Legend’ badge by becoming the most frequent or fast rider/runner on a specific route. It highlights an achievement linked to the community, and gives them a reason to keep checking the app and building that habit. Strava even notifies users when they’re knocked off the top spot, creating an engagement loop between the entire user base competing with one another.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1a870be066890199c7be9622037556cfa16ddaab-763x800.png" alt=""/></figure>
<p>People stick with products that reflect who they want to be. Subscription products that make users feel like they belong to something tend to retain far better than those that treat subscribers purely as revenue lines.</p>
<h2>If they’re engaged, just focus on the moments that matter</h2>
<p>It would be wrong to end without saying this: more messaging isn’t the answer. While filling the silence is effective, it only works if there is still breathing space. The goal isn't to fill every day with a notification — it's to show up at the right moments with the right thing.</p>
<p>If your subscribers are actively engaged with your product, the best thing lifecycle messaging can do is get out of the way:</p>
<ul>
<li>Suppress messages when users are already in a healthy habit</li>
<li>Avoid interruptions during focused sessions</li>
<li>Step back when the product is working</li>
</ul>
<p>These are all signs of a mature retention program, not a passive one.</p>
<p>The target is <strong>the moments when value is at risk of being forgotte</strong>n: right after someone converts, when <a href="https://www.revenuecat.com/blog/growth/how-to-spot-churn-before-it-happens/">engagement starts to slip</a>, when renewal is approaching. Those are the windows where a well-timed message can genuinely change the outcome.</p>
<p>Silence at the right time is fine. Silence right after someone hands over their credit card details is where early churn gets built.</p>
<aside class="tip"><strong>Start speaking to users at the right moment </strong><p>Alice’s StartApp School course reveals what to say (and when) across onboarding, trials, subscribers, drifting users, and win-backs — using segmentation, timing, and metrics to drive revenue.</p>
<p><a href="https://www.startapp.school/courses/lifecycle-marketing-for-apps">Enroll for free →</a>
</p></aside>
<h2>Treat conversion as the beginning of the relationship</h2>
<p>The shift in thinking here isn't complicated, even if building it out takes time. Treat conversion as the beginning of a new relationship, not the end of a funnel. Ask yourself: what does a new subscriber need to experience in their first week to feel confident they made the right call?</p>
<p>Start small. A warm premium welcome, a prompt toward the first premium win, and a week-one check-in is already meaningfully better than the silence most apps currently offer. Measure early cancellation rates in the first 30 days. Track whether new subscribers are actually reaching premium features in their first week. See what moves, and take it from there.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Give your next campaign a better path to purchase with RevenueCat Funnels]]></title>
      <link>https://www.revenuecat.com/blog/growth/give-your-next-campaign-a-better-path-to-purchase-with-revenuecat-funnels</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/give-your-next-campaign-a-better-path-to-purchase-with-revenuecat-funnels</guid>
      <pubDate>Wed, 09 Sep 2026 20:57:22 GMT</pubDate>
      <dc:creator><![CDATA[Courtney Touchstone]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[RevenueCat Funnels now offers smarter AI editing, ready-made bottom sheet and loader components, Firebase Authentication, and custom code to help turn campaign traffic into guided journeys that convert.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/a22ed4fe1102834eb6056ec037d1ec5b883770ca-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Paid ads and creator partnerships introduce your app to potential customers before they reach an app store. A <a href="http://revenuecat.com/blog/growth/web-to-app-funnels/">web-to-app funnel</a> lets you continue that conversation with a journey built around the message that brought them there, optimized for conversion.</p>
<p>You can explain your app’s value, learn what each visitor wants, guide them toward a relevant offer, and let them purchase before opening the app.</p>
<p>For developers, the opportunity is straightforward: test whether a guided web journey can bring in more paying customers without adding a full web growth stack to your roadmap.</p>
<p><a href="https://www.revenuecat.com/docs/tools/funnels">RevenueCat Funnels</a> keeps that test focused. Build and host the journey in RevenueCat, connect it to the products and entitlements you already manage, and measure how campaign traffic moves from its first click toward purchase.</p>
<p>Several recent updates make it faster to get from an idea to a useful first test.</p>
<h2><strong>Get to a credible first design faster with AI</strong></h2>
<p>A blank screen creates work before it creates information. You need to choose a structure, write the copy, arrange the components, and make the result look like it belongs to your app.</p>
<p>RevenueCat’s AI Editor helps you get through that starting work faster. Describe the funnel screens you want in natural language, and the editor can create or modify it directly in the dashboard. You can also provide a screenshot or a design specification to steer the result.</p>
<p>The latest editor is better at:</p>
<ul>
<li>Creating polished starting points from open-ended prompts</li>
<li>Handling complex layout and content changes</li>
<li>Interpreting requests that don’t specify every design decision</li>
<li>Rewriting copy and adjusting typography, spacing, colors, and imagery</li>
<li>Adding, removing, and rearranging components</li>
</ul>
<p>You still control the result in the visual editor. AI gets you to a credible draft faster; you decide what’s ready to test.</p>
<p><a href="https://www.revenuecat.com/docs/tools/funnels/creating-funnels">Learn more about building and editing Funnels &gt;</a></p>
<h2><strong>Add familiar interactions without configuring them from scratch</strong></h2>
<p>A useful funnel often needs more than static text and a purchase button. It may need to compare plans, prepare a recommendation, or make a short processing step feel intentional.</p>
<p>RevenueCat Funnels now includes a dedicated bottom sheet component with a ready-to-customize ‘View all plans’ example. You can keep the main screen focused on a recommended offer while still letting visitors inspect the other available plans.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/f79cb327df2b9f2e0b19a78ac3f7abbc75cc3fa1-1145x1374.gif" alt="bottom sheet component"/></figure>
<p>New loader components help you create processing and recommendation screens with:</p>
<ul>
<li>Circular or linear progress animations</li>
<li>Live percentage indicators</li>
<li>Changing status messages</li>
<li>Multi-stage loading sequences</li>
<li>Components that appear at a specified point</li>
</ul>
<p>A timer-complete trigger can automatically move the visitor to the next step. Alternatively, you can reveal a button and let them continue when they’re ready.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ec2cef13b2c39f6107a89a8eace07d18b04b18f8-400x795.gif" alt="loader component"/></figure>
<p>These additions reduce the amount of layout and interaction work required to make a funnel feel complete.</p>
<p><a href="https://www.revenuecat.com/docs/tools/funnels/creating-funnels#loader-components">See how loader components work &gt;</a></p>
<h2><strong>Use your existing Firebase Authentication setup</strong></h2>
<p>If your app uses Firebase Authentication, new customers can now create an account without leaving the funnel or passing through a separate login page. This gives you an email for lifecycle messaging if they don’t purchase and ensures customers who do purchase can sign in to the same account when they open your app.</p>
<p>Add an authentication step, connect your Firebase project, and choose between:</p>
<ul>
<li><strong>Google sign-in:</strong> visitors authenticate through a Google popup inside the funnel</li>
<li><strong>Email and password:</strong> visitors use a customizable login and sign-up screen, with optional email verification</li>
</ul>
<p>Firebase connections are saved at the RevenueCat project level and can be reused across all funnels.</p>
<p>Visitors who already have an authenticated session automatically advance through the step. Everyone else can create an account or sign in, then continue through the journey and arrive at checkout with a consistent identity.</p>
<p><a href="https://www.revenuecat.com/docs/tools/funnels/creating-funnels#using-firebase-authentication">Configure Firebase Authentication in RevenueCat Funnels &gt;</a></p>
<h2><strong>Create your own custom components</strong></h2>
<p>The standard components and AI editor will cover most first funnel experiments, but if you need to go further, the new custom component lets you (or your agent) build a component directly from web code.</p>
<p>You can package HTML, CSS, and JavaScript and place it inside a funnel screen. Custom components can support:</p>
<ul>
<li>Lottie or Rive animations</li>
<li>Interactive comparison tables</li>
<li>Accordions and custom layouts</li>
<li>Calculators, charts, or counters</li>
<li>Canvas and WebGL experiences</li>
<li>Other app-specific visuals and interactions</li>
</ul>
<p>Download the starter project, customize it yourself or give the folder to a coding agent, and upload the completed ZIP file. RevenueCat validates and hosts the bundle.</p>
<p>Packages, purchase buttons, and other purchase-critical elements remain native to RevenueCat. You use custom code for the part that differentiates the experience while keeping the surrounding journey inside the funnel editor.</p>
<p><a href="https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls/components#custom-component">Learn how custom components work &gt;</a></p>
<h2><strong>Give campaign traffic a clearer path to purchase</strong></h2>
<p>Together, these updates make the time you spend in RevenueCat Funnels go further: AI gets you to a strong first draft, ready-made components cover common interactions, Firebase connects your existing authentication, and custom components let you extend the experience with your own HTML, CSS, and JavaScript.</p>
<p>If you’re already sending traffic from paid ads or creator partnerships, funnels give you a focused next step: turn those clicks into a guided journey from campaign promise to purchase.</p>
<p><a href="https://app.revenuecat.com/">Create your first funnel in RevenueCat &gt;</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Start at the finish line: why you should design your activation journey backwards]]></title>
      <link>https://www.revenuecat.com/blog/growth/hard-paywall-activation-journey</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/hard-paywall-activation-journey</guid>
      <pubDate>Tue, 08 Sep 2026 12:28:55 GMT</pubDate>
      <dc:creator><![CDATA[Ethan Garr]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[How to reverse-engineer loyal users]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f7ca3943888dcd1c0e03de59bcae5f5136bf0fa7-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>For years, app growth followed a relatively straightforward path:</p>
<p><strong>Promise value → onboard users → prove value → present upgrade</strong></p>
<p>You introduced users to your product, helped them experience an <em>aha!</em> moment, built trust, then asked them to pay. But today, many of the fastest-growing apps have flipped the last two steps of that model:</p>
<p><strong>Promise value → onboard users → present upgrade → prove value</strong></p>
<p>The change didn't happen overnight. As apps became better at contextual onboarding, personalization, and paywall optimization, they discovered users were often willing to purchase (or at least start a trial) before they had fully experienced the value being promised.</p>
<p>When Sean Ellis and I interviewed the co-founder of Noom on <a href="https://seanellis.substack.com/p/from-onboarding-to-healthy-habits-ff9">The Breakout Growth Podcast</a> back in 2021, we were blown away <a href="https://www.revenuecat.com/blog/growth/web-to-app-onboarding-funnel">by the app's 60-step onboarding flow</a>. Today, 60 steps doesn't even sound that long.</p>
<p>And the data supports the shift: RevenueCat's <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps 2026</a> report found that <strong><a href="https://www.revenuecat.com/blog/growth/hard-paywall-vs-freemium/">hard paywalls outperform freemium</a></strong><strong> by nearly 5x on conversion</strong>.</p>
<p>For developers, this creates incredible leverage. Faster payback periods mean better <a href="https://www.revenuecat.com/glossary#return-on-ad-spend-roas">ROAS</a>. <a href="https://www.revenuecat.com/blog/growth/creative-fatigue-mobile-apps-roas">Better ROAS</a> means more dollars to reinvest in growth. Entire categories of apps are suddenly able to scale acquisition more aggressively.</p>
<p>But there’s a problem.</p>
<h2>Hard paywalls and supersized expectations</h2>
<p>When users pay before they experience meaningful value, the post-purchase user experience has to do much heavier lifting — and with a smaller margin for error to keep users from churning.</p>
<p>A simple switch in the acquisition flow means that early experience now has to:</p>
<ul>
<li>Validate the expectations<strong> </strong>created by your marketing</li>
<li>Justify the purchase decision</li>
<li>Create enough momentum for the user to return again</li>
</ul>
<p>In other words, your app still has to build a habit and activate the user, but it needs to do this all while <em>also </em>proving it is worth their time at all.</p>
<p>AI is amplifying this challenge; they monetize incredibly well, but many <a href="https://www.revenuecat.com/blog/growth/ai-app-retention-study">AI-powered apps are struggling with long-term retention</a> as their novelty fades. However, I don't think this is really an AI problem, I think it's a value problem. <strong>Building apps is getting easier, building durable growth is getting much harder.</strong></p>
<p>When users buy before they've experienced meaningful value, identifying the experiences that will actually drive long-term retention becomes more challenging, just when it becomes more important. And, unfortunately, that's where many teams get stuck.</p>
<p>A few weeks ago, an app founder reached out to me after getting enough annual renewal data to realize his renewal numbers were way below expectation. They’ve cracked the code on acquisition — their onboarding is brilliant, with over a million downloads and ~30% month-over-month ARR growth — so they have a lot to celebrate. But not enough of the users they convert are sticking around. Solving this <a href="https://www.revenuecat.com/blog/growth/first-renewal-churn">renewal curse</a> is going to be crucial for their success.</p>
<p>This is where teams often struggle. On seeing those numbers, the immediate responses are often:</p>
<ul>
<li><a href="https://www.revenuecat.com/blog/company/win-back-campaigns-for-web">Winback</a> offers to salvage churned users</li>
<li>Emails and notifications to engage users at funnel dropoff points</li>
<li>New monetization tactics to get people paying longer</li>
</ul>
<p>All good ideas, but none of them really address the underlying problem. And if these tactics quietly become the strategy, the team will never solve the value issues holding them back. The most successful growth teams I've worked with do something different. Before they jump into solutions, they work backwards, investing time into defining what success actually looks like.</p>
<h2>Value isn't a moment, it's a journey</h2>
<p>As humans, we are wired to solve problems at the point where they become visible. If retention is struggling, teams look at churn. If conversions are down, teams dive into <a href="https://www.revenuecat.com/blog/growth/paywall-conversion-boosters">paywall optimizations</a>. If engagement is soft, we cue the comms team to add more emails and push notifications. The instinct makes sense; go where the pain is. But the cause of the pain isn’t always where you feel it.</p>
<p>Value isn't created in a single moment — it's built across the entire arc of the user experience. From the first ad someone sees, through <a href="https://www.revenuecat.com/blog/growth/fix-onboarding-funnels/">onboarding</a> and early use, to the first moment a user realizes this app meaningfully impacts their life. These steps are symbiotic..</p>
<p><a href="https://www.revenuecat.com/blog/growth/product-market-fit-subscription-apps/">Product-market fit</a> is a <strong>try → use → love → keep using</strong> journey. Each step is deeply connected to the one before it, and a break anywhere in the chain can show up later as a retention problem.</p>
<p>Working backwards flips that. Instead of starting with the problem, you <strong>start with the desired outcome and ask: what journey creates that outcome</strong>?</p>
<p>Instead of asking &quot;what does a successful user look like a year from now?&quot; challenge your team to ask, &quot;if a user loves this product a year from now, what happened along the way that made them love it?&quot;</p>
<p>At each step:</p>
<ul>
<li>What did they discover?</li>
<li>What did they feel?</li>
<li>What did they start doing differently?</li>
<li>What made them realize this app was actually going to meaningfully change or improve something for them?</li>
</ul>
<p>Within the answers to those questions is your treasure map.</p>
<h2>Why starting from the finish line works</h2>
<p>A few years ago, I started running a workshop exercise called the ‘Perfect Customer Loop’ with growth teams. The goal was to make working backwards easier and more concrete.</p>
<p>It starts by picturing a must-have user of your app. Let's call her Maggie. She loves your product so much that when a friend mentions a problem your app solves, Maggie lights up and says &quot;you have got to try this&quot;. That moment of advocacy is your destination, and from there, the team works backwards.</p>
<p>In the exercise, we go around the room and ask questions like:</p>
<ul>
<li>What happened an hour before the conversation that reminded Maggie how much value she gets from your product?</li>
<li>What occurred a week ago that deepened her emotional connection to it?</li>
<li>What surprised her a month ago?</li>
<li>When was the first moment she knew this was going to work?</li>
</ul>
<p>The team works all the way back to the same &quot;you have got to try this&quot; moment where Maggie was introduced to the app by another must-have user.</p>
<p>The moments the team surfaces are informed hypotheses, and that's fine. The exercise isn't about getting it perfect, it's about <strong>creating a shared map of the value experience</strong>. You can validate the moments later through user interviews and retention data.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/0e5a87dedb3e2dca96121e245ba5e2b371a91c82-2048x2048.png" alt=""/><figcaption>Breakout Growth's Perfect Customer Loop template</figcaption></figure>
<p>I've run this workshop at RevenueCat's <a href="https://appgrowthannual.com/">App Growth Annual</a> conference and with teams across industries and categories. It consistently surfaces <strong>the gaps between the value teams </strong><em><strong>think</strong></em><strong> their app is delivering and the value users are likely to </strong><em><strong>actually experience</strong></em>. More importantly, it unlocks how your most successful users came to love your product — what they knew, what they felt, and what they experienced along the way. That's the treasure map. Find the success story, and recreate it.</p>
<p>A company I worked with connects entrepreneurs with domain experts for paid advisory sessions. The team had built their entire growth narrative around this value of access to experts, faster decisions, and better outcomes.</p>
<p>But when we ran the Perfect Customer Loop exercise, what kept surfacing wasn't any of that, it was <em>confidence</em>. Customers weren't coming back because they got good advice, they were coming back because the conversations made them feel like they could trust their own judgment.</p>
<p>That's a completely different product story compared to the value the growth team were selling.It informs everything from what your onboarding needs to communicate, to what your activation experience should feel like, to what your <a href="https://www.revenuecat.com/blog/engineering/apple-retention-messaging-api/">retention messaging</a> should say.</p>
<p>The takeaway is this: <strong>you can't design for an outcome you haven't clearly defined</strong>, and working backwards forces you to define it. When teams follow this path, the conversation almost always arrives at activation, where promise and value delivery converge.</p>
<h2>Activation is a story, not just a moment in time</h2>
<p>Ask any growth team what the most important retention driver is and they'll likely land on activation.</p>
<p>They're right, but it's worth being precise about what activation actually means. There are real moments that matter, like the first time a user experiences genuine value, and the point where that value turns into habit, but activation isn't just a completed checklist, a logged workout, or an uploaded file. Those can be critical actions and <em>aha!</em> moments, but to me, <strong>activation is a story that often starts even before a user gets to the app store</strong>.</p>
<p>In Daphne Tideman’s blog <em><a href="https://www.revenuecat.com/blog/growth/activation-metrics/">Why most activation metrics don’t predict who will stay (and what to use instead)</a></em>, she does a great job of explaining how easy it is to optimize for the wrong thing. We often conflate engagement for activation, rather than focusing on metrics that actually indicate users moving toward becoming a long-term subscriber — like ‘time to first value’ and ‘time to core value’..</p>
<p>By the time a user hits whatever you've designated as your activation event, they've already been shaped by the recommendation of a friend, an ad, your App Store listing, the onboarding experience, and even your pricing and paywall. If activation feels underwhelming, the problem is often upstream. A promise that was too big, targeting that brought in the wrong people, onboarding that set the wrong expectation.</p>
<p>When you have a hard paywall, this gets amplified. You've asked users to pay <strong>before they've experienced meaningful value</strong>, which means the first motion where you deliver value <em>really</em> needs to land.</p>
<p>If you want to design that entire activation experience well, start working backwards from what your best users already know, feel, and do.</p>
<h2>Find your must-have users, then follow them</h2>
<p>Working backwards isn't something you do once in a workshop and check off the list. It's a way of operating. Here's where to start.</p>
<p>The first step is identifying your ‘must-have’ users. Not necessarily your most active users or your highest <a href="https://www.revenuecat.com/glossary#ltvcac-ratio">LTV</a> segment, but the users who love your product and would be very disappointed if they couldn’t use it anymore.</p>
<p>Sean Ellis built his <a href="https://www.revenuecat.com/blog/growth/pre-product-market-fit-metrics/">product-market fit</a> survey to surface those users: respondents who would be ‘very disappointed’ to lose your app are the must-have users you want to make more of. They've already made the journey you're trying to design for everyone else, and the rest of the survey tells you why they feel the way they do.</p>
<p>The second step is understanding what actually happens to them along the way. This is where the work pays off. Identify the must-have users and dig into the journey that creates them. Talk to them. Survey them. Find out what they knew, what they felt, and what they did in the early days that set them up for success.</p>
<p>The Perfect Customer Loop is designed to help you do this. Sean and I wrote a guide to <a href="https://seanellis.substack.com/p/unlocking-advocacy-growths-force">using the PMF survey and the PCL together</a> if you are looking for a deeper playbook.</p>
<p>The third step is the gap analysis. Not &quot;where are users dropping off?&quot; but &quot;where does our current experience fall short of the journey that creates must-have users?&quot; That question points you toward the moments that actually determine whether users succeed, long before a retention metric surfaces the problem.</p>
<h2>Working backwards scales</h2>
<p>Retention is the obvious place to apply this mindset, but the approach is transferable across your entire growth engine:</p>
<p>For <strong>onboarding</strong>: instead of asking &quot;how do we reduce drop-off in step three?&quot; ask &quot;what does a new user need to know, feel, and believe by the end of onboarding to be set up for their first real value moment?&quot; The answer often lives earlier in the journey than you'd expect.</p>
<p>On <strong>churn</strong>, instead of building winback campaigns that depend on discounts to claw back lost users, ask &quot;what moment in the journey would have changed this outcome?&quot; Churn analysis done this way becomes an onboarding and activation roadmap, not a rescue operation.</p>
<p>On <strong>features</strong>, before building anything, ask &quot;does this move more users toward the must-have experience, or does it move us sideways?&quot;</p>
<p>The teams that build long-term growth aren't better at reacting to problems. They're better at defining success before they start designing journeys to get there.</p>
<p>Building has never been faster or easier, but the foundation hasn't changed. Users have to try your product, use it, love it, and keep using it. Working backwards from that outcome and understanding deeply who your best users are, what made them that way, and how to design more of that journey is how you turn <em>fast</em> growth into <em>sustainable</em> growth.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Adding Rive and Lottie Animations to Android Paywalls]]></title>
      <link>https://www.revenuecat.com/blog/engineering/paywalls-custom-anims</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/paywalls-custom-anims</guid>
      <pubDate>Mon, 07 Sep 2026 00:35:05 GMT</pubDate>
      <dc:creator><![CDATA[Jaewoong Eum]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[This article covers how Rive and Lottie bundles work, what changes in your Android code, and the key constraints to know before choosing one.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f2ad44b25bfa49e345eacdeb3d6c18df6f98ab0d-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>RevenueCat Paywalls gives you a component tree to build with: stacks, text, buttons, images, video, carousels, timelines. It covers the layouts most paywalls need, and the whole tree is defined on the server, so you change it from the dashboard without shipping an app release. But the vocabulary is fixed. When you want a Rive character reacting through a state machine, or a Lottie celebration that fires the moment a trial starts, no component expresses it. Custom components close that gap by reserving a box on the paywall and letting you decide what goes inside it.</p>
<p>In this article, you'll explore where the component vocabulary runs out, how the Rive and Lottie bundles are structured and the two decisions that make them work, what the upload step involves, what changes in your Android code, and the constraints worth knowing before you commit to one.</p>
<h2><strong>Where the component vocabulary stops</strong></h2>
<p>Every component in a Paywalls tree is something the SDK knows how to interpret. A <code>stack</code> arranges children, a <code>text</code> renders a localized string, an <code>image</code> draws a remote asset. Your paywall config is a composition of these known types, which is exactly why it can be served as JSON and rendered by a binary that has never seen that particular paywall.</p>
<p>The consequence is that you can compose the vocabulary, but you cannot extend it. An animation format is not expressible as a stack of text and images. Before custom components, you had three options:</p>
<ul>
<li><strong>Export the animation as a video.</strong> The <code>video</code> component gives you motion with no bundle to build, and for a fixed clip it is still the right answer. It cannot react to state, and a video costs far more bytes than the vector data that produces the same motion.</li>
<li><strong>Ship a native Composable and gate it.</strong> You write the animation in your app, then guard it behind a flag or an offering identifier. This works, but it needs an app release for every change, which gives up the reason you moved to server driven paywalls.</li>
<li><strong>Fall back to a static image.</strong> Simple and immediate, and it throws away the motion entirely.</li>
</ul>
<p>A custom component targets the case none of those cover: content that is interactive, vector based, and changeable from the dashboard. The RevenueCat docs name the use case directly, describing custom components as usable &quot;for interactive elements such as Lottie or Rive animations and animated backgrounds.&quot;</p>
<h2><strong>What a custom component is</strong></h2>
<p>A custom component is a folder of web files with <code>index.html</code> at its root. You zip that folder and upload it to the component in the dashboard. RevenueCat validates the archive, unpacks it, and serves the files over HTTPS from a subdomain dedicated to that upload. In the tree, you place it and size it the way you place any other component.</p>
<p>Three properties shape how you build one:</p>
<ul>
<li><strong>It is self contained.</strong> Everything the bundle needs travels inside the zip: scripts, styles, fonts, animation data, and the animation runtime itself. Once the bundle has loaded there is no further third party fetch, so the animation cannot fail because some CDN was slow.</li>
<li><strong>It runs under a strict Content Security Policy.</strong> This is the rule that shapes all the code below. The bundle cannot call <code>fetch()</code>, cannot use <code>eval()</code> or <code>new Function</code>, and cannot use inline <code>&lt;script&gt;</code> blocks. These are hard failures, not degradations.</li>
<li><strong>It is decorative.</strong> The docs are explicit that &quot;paywall elements such as packages and purchase buttons must remain native.&quot; Selection and purchase stay on real components, where the SDK owns the purchase path.</li>
</ul>
<p>Inside the box you are not bound by the component vocabulary. Any markup, any animation runtime, any layout technique the bundle rules allow. What you give up is the constraint being removed entirely: it is traded for a smaller set of rules, collected in the last section.</p>
<p>Two authoring requirements are easy to miss. The bundle must fill its frame, which means sizing to 100% width and height with <code>margin: 0; padding: 0</code> and no hardcoded pixel dimensions. And <code>index.html</code> must contain a <code>&lt;head&gt;</code>, because RevenueCat injects its content SDK there at upload time. You do not add that script tag yourself.</p>
<p>One more thing about placement. A custom component's visibility can be varied by condition through the override system, but its size cannot. Since framing depends on the ratio of the box to the animation, pick a size that frames acceptably in every configuration you support.</p>
<p>The CSP rule is the one to internalize first, because nearly every Rive and Lottie tutorial loads its animation by URL or file path. A bundle cannot fetch anything at display time, so both examples below are built around handing the animation data to the runtime directly.</p>
<h2><strong>Rive: A state machine driven character</strong></h2>
<p>Rive is a runtime for interactive vector animation. What distinguishes it from most animation formats is the state machine: rather than playing a fixed timeline, a <code>.riv</code> file defines states and transitions, so a character can idle, react, and settle back. Files are compact, usually tens of kilobytes.</p>
<p>The bundle for the Marty example is six files:</p>
<pre><code>rive-03-marty/
├── index.html
├── styles.css
├── app.js
├── config.js
├── rive.js
└── riv-data.js</code></pre>
<p><code>rive.js</code> is the Rive runtime and <code>riv-data.js</code> holds the <code>.riv</code> file as a base64 string. <code>config.js</code> carries the per animation settings, which lets one template serve several bundles. The snippets below inline those values instead, so each one reads on its own.</p>
<p>The interesting part of <code>index.html</code> is the script order:</p>
<pre><code>&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;utf-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;./styles.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id=&quot;stage&quot;&gt;&lt;canvas id=&quot;canvas&quot;&gt;&lt;/canvas&gt;&lt;/div&gt;

  &lt;script src=&quot;./rive.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./riv-data.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./app.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>Every script is a file reference, since inline blocks are rejected. The stylesheet is what gives the stage a height, and without it a canvas collapses to nothing:</p>
<pre><code>html, body { margin: 0; padding: 0; width: 100%; height: 100%; }
#stage { width: 100%; height: 100%; }
#canvas { display: block; width: 100%; height: 100%; }</code></pre>
<p>Two decisions are specific to shipping Rive this way. The first is which Rive build to use. The default <code>@rive-app/canvas</code> build fetches its WebAssembly from a CDN at startup, which the CSP blocks, so use <code>@rive-app/canvas-single</code> instead. That build inlines the WebAssembly into the JavaScript file, which is also why it is around 1.8 MB.</p>
<p>The second is how the animation reaches the runtime. Rive's <code>src</code> option takes a path, and loading it triggers a fetch. So <code>riv-data.js</code> carries the bytes as base64:</p>
<pre><code>window.__RIV_B64 = &quot;UklWRQ...&quot;;</code></pre>
<p>You generate that from the .riv file with one command:</p>
<pre><code>printf 'window.__RIV_B64 = &quot;%s&quot;;\n' &quot;$(base64 -i marty.riv)&quot; &gt; riv-data.js</code></pre>
<p>In app.js, decoding base64 back to bytes uses atob, which returns a string of character codes that you copy into a typed array:</p>
<pre><code>var bin = atob(window.__RIV_B64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i &lt; bin.length; i++) {
  bytes[i] = bin.charCodeAt(i);
}</code></pre>
<p>Those bytes go to the runtime through buffer rather than src. Fit and Alignment control framing, which the next section covers:</p>
<pre><code>var r = new window.rive.Rive({
  buffer: bytes.buffer,
  canvas: document.getElementById('canvas'),
  autoplay: true,
  layout: new window.rive.Layout({
    fit: window.rive.Fit.Cover,
    alignment: window.rive.Alignment.TopCenter
  }),
  onLoad: function () { start(r); }
});</code></pre>
<p>onLoad fires once the file is parsed, which is the first point where you can ask what the file actually contains. That matters because autoplay only covers files holding a single linear animation. When a file has a state machine, autoplay starts one of the linear clips instead of the intended behavior, so you stop that playback and start the machine:</p>
<pre><code>function start(r) {
  r.resizeDrawingSurfaceToCanvas();
  var machine = (r.stateMachineNames || [])[0];
  if (machine) {
    r.stop();
    r.play(machine);
  }
}</code></pre>
<p>resizeDrawingSurfaceToCanvas() matches the canvas backing store to its display size, so the result stays sharp on high density screens.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/522a548d1a1b325207cf3fda725117b9e5916bcd-340x340.gif" alt=""/></figure>
<h3><strong>Fit and alignment: Matching the artboard to the box</strong></h3>
<p>Framing is the detail that needs attention. A Rive artboard, the fixed size canvas the animation was authored on, has its own aspect ratio, and the box the paywall gives you probably has a different one. With <code>Fit.Contain</code> the artboard is scaled to fit inside the box, which leaves transparent bars on two sides. If the artboard has its own background, those bars read as unwanted padding.</p>
<p>Which setting is right depends on the artboard:</p>
<ul>
<li><strong>Transparent artboard</strong>: keep <code>Contain</code>. The bars are invisible against the paywall background, which is what you want for a mascot sitting on your own backdrop.</li>
<li><strong>Artboard with its own background</strong>: use <code>Cover</code> so the artboard fills the box, and set <code>alignment</code> to control what gets cropped.</li>
</ul>
<p>That second point matters for characters. <code>Cover</code> crops to fill, and the default center alignment crops evenly from both edges, which in a wide box takes the top of the head. Marty uses <code>Cover</code> with <code>TopCenter</code>, so a wide box crops the legs and keeps the face.</p>
<h2><strong>Lottie: A vector animation from a JSON document</strong></h2>
<p>Lottie takes a different approach. A Lottie animation is a JSON document describing shape layers, transforms, and keyframes, and the runtime interprets it into vector output. There is no state machine, just a timeline you play, loop, or seek.</p>
<p>The Premium Crown bundle mirrors the Rive one:</p>
<pre><code>lottie-02-premium-crown/
├── index.html
├── styles.css
├── app.js
├── config.js
├── lottie.min.js
└── animation.js</code></pre>
<p>Here lottie.min.js is the Lottie runtime and animation.js holds the animation JSON. Two notes on those files. The runtime is the light build, which ships without the expression evaluator and therefore without the eval call the full build contains, making it the one to pick for a bundle. And the markup differs slightly from the Rive case, since the SVG renderer needs a plain container rather than a canvas:</p>
<pre><code>&lt;body&gt;
  &lt;div id=&quot;stage&quot;&gt;&lt;div id=&quot;host&quot;&gt;&lt;/div&gt;&lt;/div&gt;

  &lt;script src=&quot;./lottie.min.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./animation.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./app.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;</code></pre>
<p>The self containment question has a simpler answer on this side. loadAnimation accepts either a path to fetch or an animationData object already in memory, so animation.js assigns the JSON to a global:</p>
<pre><code>window.__ANIM = { &quot;v&quot;: &quot;5.12.2&quot;, &quot;fr&quot;: 60, &quot;w&quot;: 512, &quot;h&quot;: 512, &quot;layers&quot;: [] };</code></pre>
<p>And app.js hands it straight over:</p>
<pre><code>var anim = window.lottie.loadAnimation({
  container: document.getElementById('host'),
  renderer: 'svg',
  loop: true,
  autoplay: true,
  animationData: window.__ANIM,
  rendererSettings: {
    preserveAspectRatio: 'xMidYMid meet',
    progressiveLoad: false
  }
});</code></pre>
<p><code>preserveAspectRatio</code> is the Lottie equivalent of Rive's fit setting, and it takes standard SVG values. <code>xMidYMid meet</code> behaves like <code>Contain</code> and fits the whole animation inside the box. <code>xMidYMid slice</code> behaves like <code>Cover</code> and fills the box, cropping the overflow, which is what you want for a full bleed animated background.</p>
<p>One habit to adopt for either runtime: pause when the paywall is not visible, so a looping animation is not drawing frames the user cannot see. The event fires when the paywall is dismissed or the app goes to the background:</p>
<pre><code>document.addEventListener('visibilitychange', function () {
  if (document.hidden) {
    anim.pause();
  } else {
    anim.play();
  }
});</code></pre>
<p>For Rive the same handler calls r.pause() and r.play() on the instance from the constructor above.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/97e7fe75b0030576f9be80e6149b254c14b1585a-340x340.gif" alt=""/></figure>
<p>he two bundles land in very different places on size. The crown zip is about 49 KB and the Marty zip about 660 KB. Both carry their own runtime, so the difference is the runtime itself: the Rive WebAssembly build is roughly ten times the size of Lottie's JavaScript renderer.</p>
<h2><strong>Uploading in the dashboard</strong></h2>
<p>Zip the <em>contents</em> of the folder, not the folder. <code>index.html</code> has to be at the root of the archive, and this is the most common thing to get wrong. On macOS, selecting the folder and choosing Compress produces the nested layout, so zip from inside the folder instead:</p>
<pre><code>cd rive-03-marty &amp;&amp; zip -r ../rive-03-marty.zip . -x &quot;.*&quot; -x &quot;__MACOSX/*&quot;</code></pre>
<p>The exclusions matter because Finder and the shell both like to add .DS_Store and __MACOSX entries, which count against the file limit and put things in your bundle you did not intend. Verify the layout before uploading:</p>
<pre><code>unzip -l rive-03-marty.zip</code></pre>
<p>If the listing shows <code>index.html</code> rather than <code>rive-03-marty/index.html</code>, the archive is right. Upload validation also rejects nested archives, duplicate paths, absolute paths, and <code>..</code> in entry paths.</p>
<p>Then add a custom component to your paywall, upload the zip, and position and size it in the tree.</p>
<p>To check your work before uploading, the starter template in the docs includes a preview script, <code>bash scripts/preview.sh</code>, which serves the folder locally and frames it at phone size. Opening <code>index.html</code> directly in a browser is quicker and will tell you whether the animation parses and plays, but it enforces none of the bundle rules, so a page using <code>fetch()</code> or an inline script will work locally and fail on the paywall.</p>
<h2><strong>What changes in your Android code</strong></h2>
<p>Almost nothing, and this is worth stating plainly because a new component type sounds like it needs new integration work. Move to <code>purchases-android</code> 10.16.0 or newer and the paywall you already present renders a custom component with no new code.</p>
<p>The Compose entry point is unchanged:</p>
<pre><code>val options = PaywallOptions.Builder(dismissRequest = { finish() })
    .build()

Paywall(options)</code></pre>
<p>The activity based path is unchanged too. The launcher takes a result handler, so the activity implements PaywallResultHandler, and it has to be created in onCreate:</p>
<pre><code>class MainActivity : ComponentActivity(), PaywallResultHandler {

    private lateinit var launcher: PaywallActivityLauncher

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launcher = PaywallActivityLauncher(this, this)
    }

    override fun onActivityResult(result: PaywallResult) {
        // handle purchased, cancelled, restored, error
    }
}</code></pre>
<p>Then present it from wherever you gate your feature:</p>
<pre><code>launcher.launch()</code></pre>
<p>Because the definition lives on the server, adding an animation becomes a dashboard change rather than a release. You can put a Rive character in front of users, measure it, and take it back out without touching your app, once your install base is on a version that knows the type.</p>
<h2><strong>Availability and constraints</strong></h2>
<p>Custom components landed in <code>purchases-android</code> 10.16.0, released on July 31, 2026, and they require Paywalls. A V1 template based paywall has no component tree to place one in.</p>
<p>That version floor is the one operational detail to plan around. An older SDK does not recognize the component and renders the component's <code>fallback</code> instead, so configure a fallback before you enable the paywall change, and treat the rollout as gated on your install base rather than instant.</p>
<p>The upload limits are:</p>
<ul>
<li>Compressed zip: 5 MB</li>
<li>Total uncompressed: 20 MB</li>
<li>Single file: 10 MB</li>
<li>Number of files: 200</li>
</ul>
<p>Rive bundles are the ones to size check, though not for the reason you might expect. Marty's <code>.riv</code> is 30 KB, and base64 encoding inflates it to 40 KB, so almost all of the 660 KB zip is the runtime. Since the runtime is a fixed 1.8 MB uncompressed, the cap that binds first is the 20 MB uncompressed total, not the 5 MB zip.</p>
<p>The bundle rules follow from the Content Security Policy, and they rule out the shape most tutorials use:</p>
<ul>
<li><strong>Reference scripts as files.</strong> Use <code>&lt;script src=&quot;./app.js&quot;&gt;</code>. Inline script blocks are rejected, even a single line of bootstrap.</li>
<li><strong>No </strong><code><strong>eval()</strong></code><strong> or </strong><code><strong>new Function</strong></code><strong>.</strong> Some library builds include an expression evaluator that uses them. Pick the build without it.</li>
<li><strong>No runtime fetching.</strong> No <code>fetch()</code>, no <code>XMLHttpRequest</code>, no <code>importScripts()</code>, and no <code>http:</code> resources. HTTPS images referenced from <code>&lt;img&gt;</code> or CSS do work, but bundling them removes the dependency.</li>
<li><strong>Relative paths only.</strong> Reference sibling files as <code>./file.js</code>.</li>
<li><strong>Keep purchase UI native.</strong> Packages and purchase buttons stay on real paywall components.</li>
</ul>
<h2><strong>Conclusion</strong></h2>
<p>Reach for a custom component when the content is the point and the vocabulary cannot express it: a branded character, a celebration on trial start, an animated backdrop. For a fixed clip, the <code>video</code> component is less work. For anything the tree already covers, use the tree, since it gets you native layout, localization, and the typed purchase path for free. When you do build a bundle, let the no fetching rule drive the design and most of the remaining decisions answer themselves.</p>
<p>What makes this worth the setup is not the animation but where the decision now lives. An animation used to be a code change, which meant a release, a review, and a staged rollout before you learned anything. Moving that box to the server turns it into something you can try on Monday and reverse on Tuesday, and that shortened loop tends to matter more than any single animation you put in the box.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Gurwi: How to turn building in public into a Shipaton #BuildInPublic win]]></title>
      <link>https://www.revenuecat.com/blog/company/gurwi-build-in-public-shipaton</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/gurwi-build-in-public-shipaton</guid>
      <pubDate>Thu, 03 Sep 2026 20:36:00 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[How building in public can get strangers to give you money to build your dreams, capture the attention of politicians, and bring loyal customers]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/1399521b77ca5618c6d21c3d6e9b3066c3b16d7a-1600x818.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p><em>As part of Shipaton, we're highlighting stories about past participants to show what's possible when you build an app. Whether you're aiming to sharpen your skills, launch your first product, grow your app, or win the whole thing, these interviews are meant to teach and inspire you.</em></p>
<p>When Captain of Shipaton, Charlie Chapman, announced the winner of Shipaton 2025’s #BuildInPublic award — a prize that included $15,000 and travel and lodging to New York — the winner was nowhere in sight. Camilo Peñalver, who had spent the previous months posting almost daily while building Gurwi, was at home watching the ceremony on his laptop.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/6580f8d12789a3bb83fdb59b625f23b2d3785217-1200x800.jpg" alt="Picture of man on stage, with a screen behind them showing an app called Gurwi"/><figcaption>Charlie Chapman announcing Gurwi as the first place winner</figcaption></figure>
<p>The choice not to be there had not been his — there had been some obstacles; a common theme in the story of Gurwi.</p>
<p>This is the story of Gurwi and Camilo, a product and its builder that have become almost interchangeable. It follows his path from a YouTube course, to an ugly viral Figma prototype, to the Gurwi. Most importantly, it is about how he built all of this in public — leading strangers to give him cash, politicians to talk with him, and millions of views on TikTok that converted into loyal Gurwi customers.</p>
<h2>Discovering Gurwi</h2>
<p>Born in Santa Marta, Camilo spent his childhood moving across Colombia's Caribbean region before finishing high school in Riohacha, the capital of La Guajira — one of the country's poorest regions. The limited educational opportunities he encountered helped shape the problem he would later try to solve. He went on to study communication, journalism, and psychology while pursuing projects in video production, fiction, and entrepreneurship.</p>
<p>That mix of education, media, and entrepreneurship eventually led him to teach himself to code and imagine Gurwi as the “Duolingo for everything”. His first public version was a crude Figma prototype, shared in a TikTok video promising a mobile app that could make education more accessible in Colombia.</p>
<p>That video reached <a href="https://www.tiktok.com/@camilopenalver/video/7368633476469280005">more than 250,000 views, 40,000 likes, and more than 500 comments</a>, proving the idea already had an audience.</p>
<h3>10 million from a stranger</h3>
<p>The video even caught the attention of Colombia’s then Minister of ICT, Mauricio Lizcano, who congratulated Camilo publicly. Camilo used the opportunity to ask for help, but the exchange produced only a short call and no practical support.</p>
<p>Yet, Camilo kept building and posting, even though development, recording, and editing left little time for anything else. In 2023, he published a <a href="https://www.tiktok.com/@camilopenalver/video/7276241682444586245">video about alleged irregularities</a> in Fondo Emprender, a government seed-capital program. It reached one million views. The following year, he applied to the same program but was rejected and was unable to apply again because no further calls for pitches were announced. Later, a Colombian-born tech entrepreneur living abroad saw <a href="https://www.tiktok.com/@camilopenalver/video/7395737752815439110">another video Camilo posted in 2024</a> and, after a 30-minute call, gave him 10 million Colombian pesos to continue building Gurwi.</p>
<p>The funding kept the project moving, but Gurwi still took almost a year to reach the app stores. The last part was a real sprint — but not because of funding, because of <strong>Shipaton</strong>.</p>
<h3>Moving from one technology to another</h3>
<p>Camilo had built the first version of Gurwi using FlutterFlow, a Flutter-based visual development platform. The stack worked great for building the MVP, but he felt that the platform was limiting some of the directions they wanted to take the app, mainly in supporting the rendering of the dynamic content that powers the Gurwi lessons.</p>
<p>Help arrived in the form of Jonnier Martínez, who joined to work on Gurwi in exchange for a small percentage of it. Since he had been programming longer than Camilo and had more experience in backend development, they decided to move from FlutterFlow to Flutter and build both iOS and Android versions of Gurwi using it, as well as a new backend and web version of Gurwi, for managing the content the app displays.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/e9654d806b3617d462fbab64e92f023d1d1dfe53-1122x1402.jpg" alt="Two men next to each other"/><figcaption>Camilo Peñalver and Jonnier Martínez</figcaption></figure>
<h2>Discovering Shipaton</h2>
<p>Camilo found Shipaton on X because he was already following RevenueCat and the wider Flutter ecosystem. Gurwi was still months from launch and the team had planned to wait until the following year, but the opportunity was too valuable to pass up.</p>
<p>The $15,000 prize could provide the starting capital Camilo had struggled to find through local programs. Winning could also help the team start a company in the United States, access Stripe, and pursue opportunities that were otherwise difficult to reach from Colombia.</p>
<h3>Shipping before they were ready</h3>
<p>Entering Shipaton meant bringing a launch planned for the following year forward by several months. Neither Camilo nor Jonnier had shipped a production app before, and the move from FlutterFlow to Flutter was still underway. They could reuse parts of the interface and the visual direction, but for example the web editor for creating lessons had to be built almost from scratch.</p>
<p>The work split along their strengths. Jonnier focused on development of the web editor, while Camilo moved between programming, strategy, marketing, recording, and editing of the mobile app. They submitted roughly three days before the deadline. The forms were familiar territory for Camilo; the harder part was presenting an app that was still new to both its users and its builders. They deliberately chose to wait until the last minute before posting the form, in order to attach the most up to date app metrics and marketing results.</p>
<h3>Building in public, in two languages</h3>
<p>Camilo had documented Gurwi in Spanish from the first rough Figma prototype. For Shipaton, he created a separate English-language account on X, where RevenueCat’s team and the judges were most active. As Jonnier and Camilo shipped features, Camilo also posted carefully edited progress videos, told the story behind the app, and tagged #Shipaton and #BuildInPublic so the work remained high on the feed.</p>
<p>The largest audience still came from Spanish-speaking social media. Camilo asked other creators to share Gurwi’s story, extending the campaign beyond what he could produce alone. Together with his own pitch, those videos generated more than four million views.</p>
<p><em>“The point is we ran. We gave it everything. My world became RevenueCat and Shipaton.”</em></p>
<p>The reach made Gurwi difficult to miss, but reach alone did not win the category. The judges chose Gurwi for the authenticity of its build-in-public story and the care visible in both the app and the videos around it.</p>
<h3>The app made it to New York first</h3>
<p>Charlie told Camilo about the win before the public announcement, since the prize included a trip to New York. Camilo already had a passport, but not a US visa, and appointments could take months or even years. After repeatedly checking for cancellations, he secured an appointment in Bogotá and the visa was approved.</p>
<p>Sadly, approval was not the same as having his passport back in hand. The Monday before the ceremony was a public holiday in Colombia, leaving too little time for the stamped passport to arrive before his flight. Camilo watched the ceremony from home while Gurwi appeared on a Times Square screen. The app had reached New York before its builder.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/067f917dfe2b53f912f0f2fdc9903244d44468b7-1200x800.jpg" alt="Photo of Times Square in night time."/><figcaption>Gurwi at Times Square</figcaption></figure>
<h2>Beyond Shipaton</h2>
<p>The $15,000 prize became the starting capital Camilo and Jonnier had spent years trying to secure. The Times Square appearance also gave Camilo a new story to share, and that story travelled through Colombian television, radio, and social media.</p>
<p>The attention converted into customers. Camilo’s later account puts Gurwi’s user payments during the year of the win at more than <a href="https://camilopenalver.com/en/me-and-gurwi/">$19,000</a>, before the app stores’ 15% commission. The team also founded Gurwi LLC, giving the team a chance to monetize with Stripe, something that was not possible in a Colombia-based company.</p>
<p>After Shipaton, Camilo and Jonnier have kept building Gurwi, growing the course catalogue, exploring B2B uses, and launching prompt-generated, hyperpersonalized classes. It’s safe to say that the journey of Gurwi is far from over.</p>
<h3>Advice for Shipaton participants</h3>
<p>For this year’s participants, Camilo’s first advice is to <strong>tell a story</strong>. A strong app is not enough on its own; builders should show why they’re making it — “try to make people connect with the project and the person itself.”</p>
<p>Nonetheless, visibility is not a substitute for purpose. Camilo rejects copying existing apps for quick money: <strong><a href="https://www.revenuecat.com/blog/growth/product-market-fit-subscription-apps/">build something original</a></strong>, or at least something rooted in a real motivation. Start with something you want to use and become your own user. This helps you understand which problems matter, and gives the product a reason to outlive Shipaton.</p>
<p>Finally, <strong>make the work visible</strong>. Throughout the campaign, Camilo documented the process, asked other creators for help, and of course: shipped. No amount of visibility can replace shipping the app, and continuing to ship features which people want.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The late submitter's guide to getting through app review]]></title>
      <link>https://www.revenuecat.com/blog/company/the-late-submitters-guide-to-getting-through-app-review</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/the-late-submitters-guide-to-getting-through-app-review</guid>
      <pubDate>Thu, 03 Sep 2026 15:57:25 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[Everything you need to get your app approved by the App Store and Play Store review before the Shipaton deadline]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/ba21ff0f2e7b9a044ac3c7e0c15eeeb9c8b53e13-1600x818.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>The late submitter's guide to getting through app review</p>
<p>Everything you need to get your app approved by App Store review and Play Store review before the Shipaton deadline</p>
<p>At the time of writing, there’s a little less than a month left before the Shipaton deadline. Following conversations in our Discord and on social media, we’ve noticed three groups of developers: some have been stuck in app review despite submitting early; some haven’t submitted yet and are worried they won’t make it; and some are only just starting to build and are considering giving up because they’ve heard reviews can take a long time.</p>
<p>So I decided to write this guide to help you get through app review as quickly as possible. Apple and Google can be unpredictable, but the process is mostly manageable if you do the right things. Even if there’s less than a month left when you read this, don’t give up: mobile apps can ship surprisingly fast. Your app must be publicly available — not just in TestFlight or a Google Play testing track — before submissions close, so review should be your priority. This guide will show you how to reduce your app to a review-ready release, navigate Google Play’s 14-day testing requirement, and avoid preventable delays. If you’re a student, you can skip the stores and enter the Next Gen category with a demo video and public open-source repository instead.</p>
<h3>How much time do you actually have?</h3>
<p><a href="https://www.revenuecat.com/blog/engineering/how-to-submit-your-app-for-shipaton">Shipaton’s submission requirements</a> say that your app must be publicly available — not just in TestFlight or a Google Play testing track — in the US app stores by September 30th at 11:45 pm PDT. Apple also says that it can take up to 24 hours for the app to appear in the App Store, so to be on the safe side, your app should be in stores, with RevenueCat powering an in-app purchase or serving ads through RevenueCat Ads, at least a day or two before the submission period ends.</p>
<h3>How does the app review process work?</h3>
<p>The next sections will assume some familiarity with the app review process both Apple and Google have for their respective stores. Fear not, we’ll go through the fundamentals here.</p>
<p>To publish your app, you need a developer account. Apple charges $99 per year, while Google Play charges a one-time $25 registration fee. You can enroll as an individual or an organization.</p>
<p>Organization accounts require additional verification, which may include a D-U-N-S number. New personal Google Play accounts also have a testing requirement, which we’ll cover below.</p>
<p>Once you have your account, you need to create your app, provide all the required assets, and bundle your app. After that, you can submit it to different “release tracks”. In Apple’s case, these release tracks are TestFlight and App Store. The first one is meant for testing your app either in private or public beta, and the latter is the actual store release, which allows everyone to download your app (and pay actual money for it).</p>
<p>Google Play offers internal, closed, open, and production tracks. You don’t have to use every testing track. However, some new personal accounts must complete a closed test before applying for production access. More on this below.</p>
<p>When we talk about submitting your app for review, we usually mean the store release tracks. Neither Apple nor Google allow apps into their store before someone from their team has reviewed the app and made sure it's not misleading, buggy, or unsafe for users. This is the part where first-time developers often get stuck. However, by doing the right things, which we go through next, you can make this process more likely to go through without problems.</p>
<h2>I have not yet submitted my app for review</h2>
<p>Ok take a deep breath, if you’re at this stage with your app, we will need to sprint a little, because our first priority will be to get our app into the review pipeline as soon as possible. To do this we need to strip our app to its core features. I’ve written about how to <a href="https://www.revenuecat.com/blog/engineering/how-to-win-shipaton-part-2-building-fast">build a minimum lovable product</a> in an earlier post, but to summarize:</p>
<p>build something that your core users would love, and drop everything else.</p>
<h3>Should you ship your first version without in-app purchases?</h3>
<p>Comment out the draft features, remove unfinished authentication if the core experience doesn’t need it, and postpone subscriber-only extras. You can also consider submitting a complete free version first and adding monetization in a second submission once the first is approved. This is optional, and it only makes sense if you have enough time for two reviews.</p>
<p>Purchases and subscriptions give reviewers more to check, and missing metadata, unclear instructions, or misconfigured products can cause a rejection. But for the main Shipaton competition, your qualifying version must still be live before the deadline with RevenueCat powering an in-app or web purchase, or serving ads through RevenueCat Ads. If the deadline is close, including monetization in the first submission may be safer than betting on a second review. Whichever route you choose, test your purchases in the <a href="https://www.revenuecat.com/docs/test-and-launch/sandbox">platform sandboxes</a> before submitting the monetized version.</p>
<h3>Ace Google’s 14-day testing requirement</h3>
<p>This requirement applies to personal Play Console accounts created after Nov 13, 2023. Before you can apply for production access, you must run a closed test with at least 12 testers opted in continuously for 14 days.</p>
<p>Start with the earliest functional version of your app. You can keep building while the closed test runs.</p>
<p>Once the 14 days are complete, apply for production access. Google may require additional testing if participation or engagement is insufficient, so encourage testers to use the app and share feedback.</p>
<p>To recruit testers, reach out to your friends and family. If you’re a billionaire in Gotham City, or just someone who doesn’t have family or friends, head to our Shipaton Discord to make use of the #looking-for-google-play-tester channel to recruit people to give your app a try.</p>
<p>My colleague Jaewoong wrote a deeper guide to <a href="https://www.revenuecat.com/blog/engineering/google-play-14-day/">Google Play’s 14-day testing requirement</a> if you want the full walkthrough.</p>
<h3>Make use of TestFlight and testing tracks</h3>
<p>Talking of testing, it is good to test your app with a closed or open group of people before submitting the app to stores. This is a great way to gather feedback on your product, but it can also help you catch what an app review might flag: bugs, performance issues, bad copy, and incomplete features. You save the reviewer’s time and have a better feedback loop in your development process.</p>
<h3>Get all your assets ready</h3>
<p>Apple and Google both have extensive guidance on how your store submission should look, what assets and things you need (e.g., privacy policy and terms of service documents), and what not to say, for example (mention the word beta, or call your unreleased app the number 1 app for doing anything). Read these carefully, and ingest the information yourself, so that when you start editing your screenshots and app descriptions, you know exactly what is allowed and what is not.</p>
<p>For more detail, read <a href="https://developer.apple.com/app-store/review/">Apple’s common app review issues</a> and our guide to <a href="https://www.revenuecat.com/blog/growth/the-ultimate-guide-to-app-store-rejections">common App Store rejection reasons</a>.</p>
<h2>I’m ready to submit / I’ve already submitted my app</h2>
<p>This section applies whether you're preparing to submit or your app is already waiting for review.</p>
<p>Before withdrawing a submission, understand what happens. Apple removes it from the review queue, and resubmitting starts the review process over. Apple does not promise to review submissions in the order received.</p>
<p>Google is more explicit: review time is counted from your latest submitted change. Sending additional changes while an app is under review may push it to the back of the review queue.</p>
<p>Don't restart review for a minor improvement. First, check what you can edit without withdrawing the build or whether you can provide the missing information through the store's review communication tools.</p>
<p>If you discover something likely to block access or cause a rejection, restarting may still be the faster choice. The following sections will help you identify those issues and give reviewers the information they need.</p>
<h3>Go through your submission</h3>
<p>If your submission is currently in review, it’s worth going through the submission fields again, seeing if you’ve missed something. Common things to check are, for example, if you placed placeholder links for a privacy policy, or if you forgot to put the actual content in that URL. In that case, you can just go and add the missing privacy policy. The same goes for your paywall and the links on it. Both Google and Apple require apps to have a privacy policy, and it needs to match what you submitted in the privacy declaration.</p>
<p>For apps with subscriptions, Apple and Google require appropriate Terms of Use links. Apple provides their own that you can link to in your App Store description by adding it at the end:</p>
<blockquote><p>Terms of Use (EULA): https://www.apple.com/legal/internet-services/itunes/dev/stdeula/

Privacy Policy: https://your-apps-privacy-policy.com</p></blockquote>
<p>A common thing with both the Terms of Service and privacy policy is that you can get by with very simple versions if you don’t add analytics or in-app purchases to your app. To get approved faster, consider dropping them as we mentioned earlier and adding them back later on.</p>
<h3>Figure out your auth</h3>
<p>If your app has authentication, check that you’ve created a test account and provided the username and password in the fields prepared for those. Reviewers won’t create accounts, but instead expect to use an account that has already been set up. Make sure that your test account does not have two-factor authentication on, as that will block the reviewer. In case your test account can’t be used without 2FA, then consider building a demo mode that shows all parts of your app, and documenting that in the review notes.</p>
<p>If your app uses Google or other third-party authentication, make sure you’ve also included Sign in with Apple, as that is a requirement. If your app uses third-party authentication to access data that is only possible with that account, for example, you built an email client for Gmail, then document that in the review notes as well.</p>
<p>If your app has subscriptions, make sure that your test account does not have an active entitlement; otherwise, the test account might not see your paywall, which in turn blocks the reviewer from reviewing your paywall and in-app purchases.</p>
<h3>Record a video of your app in use</h3>
<p>There’s been an uptick in Apple asking people to record a video of their app in use, showcasing, for example, the in-app purchases and authentication parts. This can be a simple screen recording, under a minute in length, that just showcases how your app functions and what happens, for example, after purchasing a subscription. Don’t overthink it, and consider adding it before reviewers even ask for it.</p>
<h3>Use the expedited review process (for bugs)</h3>
<p>This final point is something we advise against. Apple allows you to request an expedited review if you face “extenuating circumstances.” In practice, this means fixing a critical bug that severely affects the app experience or coordinating a release with an event you’re directly associated with.</p>
<p>You might think that releasing your app for Shipaton is one of these events where you can use the expedited review process to get your app approved in time, especially since Shipaton is a time-sensitive event and you are directly associated with the event as a registered participant.</p>
<p>However, Apple does not explicitly say that hackathon or competition timelines qualify. Apple also decides each request individually. RevenueCat’s official guidance is not to use expedited review for this. Instead, focus on submitting your app early enough and following the tips above.</p>
<h2>What your next 30 days should look like</h2>
<p>The points we’ve gone through in this article are not a magic bullet for acing app store reviews, but following them multiplies your chances for having a compliant app, the necessary requirement for passing the store review.</p>
<p>In terms of timeline, you should focus on getting your app in review in the next two weeks, and preferably in the next week. Stop developing new features. Instead, focus on fixing launch-blocking bugs and getting your app to an MVP stage you can submit for review. Once the app is approved by Apple or Google, the review process tends to go faster, so you will have a higher chance of getting, for example a version with in-app purchases approved.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Beyond the building blocks: custom components for Paywalls and Funnels]]></title>
      <link>https://www.revenuecat.com/blog/company/beyond-the-building-blocks-custom-components-for-paywalls-and-funnels</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/beyond-the-building-blocks-custom-components-for-paywalls-and-funnels</guid>
      <pubDate>Wed, 02 Sep 2026 17:32:07 GMT</pubDate>
      <dc:creator><![CDATA[Courtney Touchstone]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[Custom components let you add self-contained HTML/CSS/JS to Paywalls and Funnels for animation and expandable content]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/703be7f7ffa311a6fe1eb0da691b66180680ef18-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Arthur C. Clarke, the British science-fiction author and futurist behind <em>2001: A Space Odyssey</em>, spent his career imagining what technology could make possible. </p>
<p>As he put it:</p>
<p>“The only way of discovering the limits of the possible is to venture a little way past them into the impossible.”</p>
<p>Even flexible visual builders have limits. You can arrange the available components in countless ways, but some ideas, such as a branded animation or expandable FAQs, don’t fit neatly within a predefined set of building blocks.</p>
<p>We want to give developers more freedom to push what their purchase experiences can do and bring their most creative ideas to life. That’s why we introduced custom components for RevenueCat Paywalls and web-to-app Funnels.</p>
<p>With custom components, you can bring self-contained HTML, CSS, and JavaScript into a paywall or funnel to create animated backgrounds and icons, accordions and expandable content, and experiences tailored to your app.</p>
<p>Purchase-critical elements still rely on RevenueCat’s native components and checkout flows. But the canvas around them just became much bigger.</p>
<p>Here are two ways to put custom components to work.</p>
<h2><strong>Bring your purchase experience to life with animation</strong></h2>
<p>Motion can add personality, reinforce your brand, and guide attention toward the most important parts of a <a href="https://www.revenuecat.com/blog/growth/paywalls-study-guide/">paywall</a> or <a href="https://www.revenuecat.com/blog/growth/web-to-app-funnels/">web-to-app funnel</a>.</p>
<p>Custom components let you create animated backgrounds, icons, illustrations, and other visual treatments that go beyond static media. You can build animations directly with CSS or JavaScript, or use libraries such as Lottie and Rive by including them in your component bundle.</p>
<p>For example, you could:</p>
<ul>
<li>Add a subtle animated background behind your value proposition</li>
<li>Bring benefit icons to life as customers explore an offer</li>
<li>Use a looping illustration to demonstrate your app’s premium experience</li>
<li>Add branded motion to connect the different steps of a web-to-app funnel</li>
</ul>
<p>Animation doesn’t need to dominate the experience. Even small moments of movement can make a paywall or funnel feel more polished and connected to the rest of your brand.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1b5b9d74a582fdaee72a120625e0b55172675f01-1000x563.gif" alt=""/></figure>
<h2><strong>Let customers expand the details they care about</strong></h2>
<p>Customers often have questions before subscribing, but putting every answer on the screen at once can make a purchase experience difficult to scan.</p>
<p>With HTML and JavaScript, you can create accordions and expandable sections that keep the initial experience focused while giving customers access to additional context.</p>
<p>You might use expandable content for:</p>
<ul>
<li>Frequently asked questions</li>
<li>More information about individual benefits</li>
<li>Explanations of how a feature works</li>
<li>Details about what’s included with an upgrade</li>
<li>Supporting information within a web-to-app onboarding journey</li>
</ul>
<p>This creates a more layered experience. Customers can quickly understand the primary value proposition, then dig deeper when they need reassurance or clarification.</p>
<p>In a paywall, expandable content can keep the offer focused without hiding useful details. In a funnel, it can add depth to an onboarding screen without turning every step into a wall of text.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/f8ca6badae21e12c30cb760e1003280aec8ad9fc-1672x941.png" alt=""/></figure>
<h2><strong>Build it with an agent or write it yourself</strong></h2>
<p>To get started, add a custom component in the RevenueCat Paywall or Funnel Editor and download the starter bundle. It includes an <code>index.html</code> entry point, a local preview, packaging tools, a getting-started guide, and instructions that coding agents can follow.</p>
<p>You can give the folder to your coding agent of choice and describe the experience you want.</p>
<p>Your coding agent can use the included instructions to build the component within RevenueCat’s requirements. If you prefer to work directly with the code, you can edit the HTML, CSS, and JavaScript yourself.</p>
<p>When the component is ready, preview it, package the files, and upload the resulting ZIP file. You can then place the custom component where you want it in your paywall or funnel.</p>
<h2><strong>Creativity, with guardrails</strong></h2>
<p>Custom components are self-contained and run inside the area they occupy in your paywall or funnel. They’re intended for decorative and supplementary experiences.</p>
<p>They shouldn’t contain interface elements required to complete a purchase. In Paywalls, packages and purchase buttons must remain native. In Funnels, custom components should complement RevenueCat’s native purchase controls and checkout flow.</p>
<p>Uploaded bundles must also follow RevenueCat’s security, file structure, and size requirements. For in-app paywalls, your app needs a supported RevenueCat SDK version to display custom components. Review the <a href="https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls/components#custom-component">custom component requirements and minimum SDK versions</a> before publishing.</p>
<p>Custom components open up a much broader creative canvas across RevenueCat Paywalls and web-to-app Funnels, from a single animated icon to an expandable experience that helps customers understand why they should upgrade.</p>
<p><a href="https://app.revenuecat.com/">Open the RevenueCat Dashboard</a> and start building.</p>
<p>We can’t wait to see what you create.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Built With Science sends 90% of its traffic to a quiz, not the App Store]]></title>
      <link>https://www.revenuecat.com/blog/growth/ethan-ethier-build-with-science-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/ethan-ethier-build-with-science-sub-club-podcast-2026</guid>
      <pubDate>Wed, 02 Sep 2026 12:59:04 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Ethan Ethier helps run a fitness app with a built-in audience of 7 million YouTube subscribers — and almost nothing about its growth playbook looks the way you'd expect.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/2df31acfe4cbcb302fef0b90a92c84847020ff1d-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Built With Science scaled Meta ads from zero to $100,000 a month in profitable spend this year. Almost none of that traffic goes anywhere near the App Store.</p>
<p><a href="https://www.youtube.com/watch?v=EyoYQU6JyV8">Watch on YouTube</a></p>
<p>&quot;About 90% of our traffic is actually going to our website, and we have a really long personalized quiz that we send all leads to,&quot; says Ethan Ethier, the company's Head of Growth and Operations. The quiz isn't a lead-capture gimmick — it does three jobs before a user ever sees a price. &quot;It educates the user on the actual product, it overcomes objections, and it increases the perceived value of the product,&quot; Ethier explains. And the answers do real product work: they build the user's personalized training plan inside the app, so the funnel doubles as onboarding.</p>
<p>They didn't take the web-first approach on faith, either. The team ran traffic head-to-head, App Store versus quiz, and the quiz won on conversion — decisively enough that direct-to-store now only makes sense for the highest-intent users who already know what they want. One wrinkle worth noting: when they rebuilt the same quiz on Android, it converted on par with the web. It was only App Store traffic that lagged.</p>
<h2><strong>The influencer app that waited a year to run ads</strong></h2>
<p>There's a persistent myth that if you have millions of subscribers, your app markets itself. Built With Science was founded with YouTuber Jeremy Ethier — Ethan's brother — whose channel has over 7 million subscribers. The app still had to be grown the hard way.</p>
<p>The app launched in early January 2025. The first Meta ad didn't run until a full year later. &quot;We wanted to double down on improving the product,&quot; Ethier says. &quot;As long as we're constantly improving our trial-to-paid and retention, that lifts the floor for everything else.&quot; The logic: organic traffic converts at your ceiling, paid traffic converts below it, so buying traffic before the floor is fixed just buys expensive proof of the same problem. Once trial-to-paid, retention, and churn looked healthy, they scaled — and today organic converts at 35–40% trial-to-paid, with colder Meta traffic around 25%, at a premium $189/year price point.</p>
<p>Just as counterintuitive: Jeremy barely appears in the ads. &quot;We're not relying on Jeremy at all for ads,&quot; Ethan says. The team wanted to solve what he calls the bus problem — everything depending on one person's YouTube channel — so the Meta program runs on B-roll, testimonials, and static creative. A channel that scales without the founder's face is a channel that actually scales.</p>
<h2><strong>The pricing page change that moved annual from 60% to 80%</strong></h2>
<p>The team's biggest win of the year didn't touch the price. Built With Science charges $189 a year or $30 a month, and the old pricing page showed both plans side by side. Roughly 60% of subscribers picked annual.</p>
<p>The winning variant showed only the annual plan upfront, paired with a day-by-day timeline of the free trial: day 1, you get your personalized plan; day 12, a reminder that your trial is about to end and you can cancel anytime. Monthly still exists behind a view-all-plans option. &quot;We didn't change the price, we didn't change the positioning. It was simply a UI shift which had a massive impact for us,&quot; says Ethan. Annual take jumped to 75–80% — at nearly $190 a year, a substantial LTV shift from a layout change that mostly worked by shrinking the perceived risk of starting the trial.</p>
<p>The same page also produced their biggest fail: an experiment that added more context to clarify the value proposition actually hurt conversion. Their read — at the moment a user is motivated enough to be looking at prices, more information is friction, not reassurance.</p>
<h2><strong>A gym buddy plan that 15% of trial starts take</strong></h2>
<p>One of the more unusual pricing mechanics in the app came straight from user requests for a family plan. During the web quiz, users can add a &quot;gym buddy&quot; — a partner who joins at 15% off for both people.</p>
<p>Around 15% of everyone who starts a trial adds one. That lifts average order value on its own, but the quieter win is retention: gym-buddy subscribers stay on the app longer, the same dynamic family-plan apps see with their highest-retaining cohorts. And because the question lives in the quiz, the pricing page stays clean for solo users — no paywall clutter for an option most people won't take.</p>
<h2><strong>70% of their experiments fail. That's the system working.</strong></h2>
<p>&quot;Probably 70% of the experiments that we run fail in the first go,&quot; Ethan says. &quot;And that doesn't mean it's a failure... often we're iterating on these failures to uncover a winner.&quot;</p>
<p>The process behind that: be obsessed with the problem, not the solution. The team starts with funnel data in Amplitude, finds the biggest drop-off, lists every plausible problem with that step before discussing a single fix, locks in the most likely problem as a team, and only then brainstorms solutions. Everything gets documented — which is what turns failures into iterations. A no-trial toggle test (pay upfront, get 20% off) initially lost; the written record pointed to copy that framed the free trial as the losing option. A rerun that made both choices feel good is now showing a strong lift.</p>
<p>In <a href="https://www.youtube.com/watch?v=EyoYQU6JyV8" target="_blank" rel="noopener noreferrer">the full episode</a>, Ethan and David also get into Jeremy AI — the in-house chat coach one engineer built in a weekend that became a customer-favorite feature — quiet feature launches that keep engagement metrics honest, and how running their own in-app training studies feeds both the science and the marketing.</p>
<p><strong>Guest links:</strong></p>
<ul>
<li><a href="https://www.linkedin.com/in/ethanethier/">Ethan Ethier on LinkedIn</a></li>
<li><a href="https://builtwithscience.com/">Built With Science</a></li>
<li><a href="https://builtwithscience.com/careers/">Built with Science Careers</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Curse of the first renewal: how to cure churn for each subscription duration]]></title>
      <link>https://www.revenuecat.com/blog/growth/first-renewal-churn</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/first-renewal-churn</guid>
      <pubDate>Tue, 01 Sep 2026 12:22:35 GMT</pubDate>
      <dc:creator><![CDATA[Daphne Tideman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[How to make the most of your activation window]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/3043aad3426ba01f7b031284a9766900c9af3310-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Over four years of university, I noticed the same pattern again and again.</p>
<p>Week 1: The lecture hall was full. 400 eager students, all willing to give the subject (and the lecturer) a chance.</p>
<p>Then Week 2 arrived. The good intentions faded, and attendance dropped to maybe 300–350. Over the course of the term, that number would slowly whittle down to 200–250, depending on the lecturer and the topic. (Statistical modeling had a particularly brutal drop-off.)</p>
<p>But never did I turn up to Week 2 and find only 140 students left. A 65% drop-off in a single week.</p>
<p>Yet that's exactly what happens with weekly subscribers. By the <a href="https://www.revenuecat.com/blog/growth/average-subscription-renewal-rates-by-app-category/">first renewal, only 35–58% remain</a>. That lecturer (your app) has to work far harder than any of mine ever did to get people back in the room. <strong>The first renewal isn't a billing event. It's the ultimate test of survival.</strong></p>
<h2>The first renewal drop-off in numbers</h2>
<p>I’d love to say that monthly and annual subscriptions are less dramatic than weekly subscriptions, but they really aren't. Here’s the drop-off for first renewal by plan, from the <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps report</a> 2026:</p>
<ul>
<li>Weekly subscriptions: 42–65%</li>
<li>Monthly subscriptions: 39–58%</li>
<li>Annual subscriptions: 60–77%</li>
</ul>
<p><em>Ouch</em>. <em>Triple Ouch.</em></p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/bbf38eabddb514702ee3617f6fd53a1bd9d60d39-1826x788.jpg" alt="chart showing first renewal by plan duration"/></figure>
<p>The durations differ, but the pattern is the same: <strong>the first renewal is where you lose the most subscribers.</strong></p>
<p>The good news? Once someone gets past that point, the drop-off slows dramatically.</p>
<p>For weekly subscribers who make it to their second renewal, 74–91% will still be paying by their third. The same pattern applies to annual plans: get someone to year two, and they’re far more likely to stay for year three, and probably year four too.</p>
<p>So, how do you survive the survival test? And why is this one moment such a big deal? Here’s my theory.</p>
<h2>You only have the activation window to convince subscribers to renew</h2>
<p>Why didn't a user renew? Was your <a href="https://www.revenuecat.com/blog/growth/web-to-app-onboarding-funnel">onboarding funnel</a> not good enough? Urgh, it was that bug the app had last week, wasn't it?</p>
<p>Maybe, but those are often just symptoms. The real reason is usually that time-to-value wasn't reached within the <a href="https://www.revenuecat.com/blog/growth/activation-metrics/">activation window</a>. Your app never truly became part of their life before the renewal moment.</p>
<p>Just like with <a href="https://www.revenuecat.com/blog/growth/7-day-trial-subscription-app/">trial lengths</a>, different subscription durations give you different activation windows to prove your value. And no, the window isn't equal to your subscription length. I believe the most important activation periods are actually:</p>
<ul>
<li>Weekly: first five days</li>
<li>Monthly: first two weeks</li>
<li>Annual: first month</li>
</ul>
<p>For weekly and monthly subscribers, <strong>the window closes when they start worrying about the next charge.</strong></p>
<p>Annual subscriptions are different. With a whole year of runway, it feels like you have plenty of time… but you don’t. If someone hasn’t activated and experienced the value of your product by Month 3, once the embedding window closes, the odds of keeping them around drop significantly.</p>
<p>We see this when people actually cancel annual subscriptions:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/7aaae84c0cc125791af8d435714794f90ddeb7d2-1593x690.png" alt="chart showing cancellation by app category"/></figure>
<p>23–50% of annual subscribers who cancel do so <strong>in the first month</strong>, with another 8–10% dropping off in Month 2. Some of those are the “I’m scared I’ll forget to cancel” group (I’ve definitely done that). But a lot of them are simply people who were never convinced. They never reached the point <strong>where the value of the app outweighed the cost</strong>.</p>
<p>This matters because winning that first renewal looks completely different, depending on which subscription duration the user signed up for. Let’s run through how to beat the first renewal curse for each plan length.</p>
<h2>The weekly subscriber: fast-tracking a habit</h2>
<p>Let’s cut to it: 42–65% of <a href="https://www.revenuecat.com/blog/growth/weekly-subscriptions">weekly subscribers</a> are gone after one billing cycle. (Again, ouch.)</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/d0603ab4c22e6a57e5030ce4c6dcc3c5e7b98a69-2048x1427.jpg" alt="chart showing first weekly renewal by app category"/></figure>
<p>As mentioned, those first five days are what matter. Time to love bomb (you have my permission). This is the moment to go all in, no playing hard to get.</p>
<h3>Day 0: love bombing and driving action</h3>
<p>It starts on Day 0 with a <a href="https://www.revenuecat.com/blog/growth/fix-onboarding-funnels">strong onboarding experience</a>. You want users to get some kind of win, progress, or sense of achievement in that first session. Don’t make them wait to feel valued.</p>
<p>The first renewal isn’t won <em>at</em> renewal. It’s won before they even convert, on that Day 0. We see this with 3-Day trials (often the trial length for weekly subscriptions, when there is one) where 84% of cancellations happen between Day 0 and Day 1.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/d5ab5b20370d20e67f212421145aec85c797a1dd-1024x618.png" alt="chart showing bar chart for trial duration cancellation day "/></figure>
<p>Which means what you show someone immediately after they pay matters a lot. The moment they become a paying customer? Hit them with a <a href="https://www.revenuecat.com/blog/growth/post-purchase-screen/">strong post-purchase screen</a> that reinforces the decision they just made, reminds them why they signed up, and gets them moving toward the next value moment.</p>
<p>For short subscriptions, I usually recommend focusing this moment on action: what is the one small, simple thing they can do right now? The goal isn’t to explain every feature. It’s to get them moving toward the value they signed up for.</p>
<p><a href="https://www.greg.app/">Greg</a>, a plant care app, does this well by inviting the user to log their plants:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/86240afebc1cf47403d55eaa52399ef26efb99f1-1121x472.jpg" alt="five screenshots showing onboarding for plant app Greg"/></figure>
<h3>Day 1–3: deepen the value</h3>
<p>From there, Days 1–3 are about deepening the value. The user should experience the app’s core action, see a result, and start building a reason to come back.</p>
<p>And because the window is so short, you can (and should) be more aggressive with communication than you would be for monthly or <a href="https://www.revenuecat.com/blog/growth/annual-subscriptions-apps-pros-cons/">annual subscriptions.</a></p>
<h3>Day 4–7: fast-track the habit</h3>
<p>Now you're trying to fast-track a habit. The data is pretty clear that real habits take longer than 7 days to form. But that's okay. You don't need a fully-formed habit yet. You just need the first signs of one: a return visit and the core action repeated enough times for the user to feel the value.</p>
<p>If you aren't sure what level of usage is ‘enough’, reverse engineer it from the subscribers who stay. What do they do in their first week? As you identify the behaviors that matter, segment users based on those actions and analyze both the frequency of the behavior and the percentage of users who complete it: that's how you find the <strong>leading indicators of renewal</strong>.</p>
<p>Throughout all of this, notifications and emails are your friend. The window is short, so don't be afraid to communicate more frequently. Just skip the generic reminder-style notifications. &quot;Don't forget today's session&quot; is really just another way of saying, &quot;Please use my app.&quot; Make your messages <strong>value-oriented</strong> instead:</p>
<ul>
<li>Celebrate what they've done</li>
<li>Show what they could achieve next</li>
<li>Introduce features that will help them get there</li>
</ul>
<p>Timing matters just as much as the message itself.</p>
<p>Duolingo tested sending re-engagement emails at different intervals and found that <a href="https://econsultancy.com/six-a-b-tests-used-by-duolingo-to-tap-into-habit-forming-behaviour/">23.5 hours after a user’s last lesson worked best</a>: just before the same time the next day, when the habit would naturally repeat. And because Duolingo has streaks, they get to say, &quot;Remember, you're on a 24-Day streak,&quot; instead of &quot;Please come back.&quot;</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b403bcae7c3da2a65d848eefc63ab7b41f5f92fa-800x645.jpg" alt="screenshots showing three notifications from Duolingo emphasizing streak ending soon"/></figure>
<p>For one client I worked with, we knew that completing a particular assessment (which includes human feedback — I know, revolutionary in this AI era) has a huge impact on both renewals and long-term retention. So our emails, push notifications, and even in-app messages all focus on getting new subscribers to complete that assessment as quickly as possible.</p>
<p>Once you know which behavior predicts retention, your job becomes much simpler: get more people to do that thing.</p>
<h2>The monthly subscriber: 30 days to routine</h2>
<p>The numbers are gentler for <a href="https://www.revenuecat.com/blog/growth/monthly-subscriptions-when-to-offer">monthly subscribers</a>, but you're still losing 39–58% at that first renewal.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/dd6fa5de0023dc092d988cba5c75d54b779e63b1-1998x1430.jpg" alt="chart showing first three monthly subscription renewals by app category"/></figure>
<p>The good news is that you have longer than a week this time. You don't need to force-feed a habit in seven days. But 30 days go by faster than you think, which means each stage of the month needs a job. Week 1 is about activation. The following weeks are about building <strong>familiarity, reinforcing value, and creating reasons to come back</strong>.</p>
<p><a href="https://subclub.com/episode/how-to-boost-retention-with-subscription-lifecycle-messaging-alice-muir-phiture">Alice Muir shared on the Sub Club podcast</a> that <a href="https://www.revenuecat.com/blog/growth/lifecycle-marketing-campaigns-optimize-revenue">lifecycle messaging</a> in this period has the highest ROI. So if you run monthly subscriptions, this is where you can focus your efforts: building out the communication and the relationship with the user.</p>
<h3>Days 1–7: get them to a meaningful outcome</h3>
<p>Not a completed tutorial, not onboarding finished, not even a streak starter. What you're looking for is <strong>a clear value moment</strong>. Someone probably won't feel fitter after a single workout or dramatically calmer after a single meditation session. But if they've achieved something, you've celebrated that progress with them, and it felt easy enough to do again? That's the beginning of a habit.</p>
<h3>Days 8–21: habit reinforcement</h3>
<p>This is where most apps go silent. I use the phrase ‘love bombing’ far too often in my work with subscription apps, but it fits. Most apps love bomb users for the first seven days and then completely forget about them. Take a look at your CRM flows. How often are you actually communicating with users in Weeks 2 and 3? The frequency can taper, but it shouldn't fall off a cliff.</p>
<p>At this stage, the goal is simple: figure out <strong>where the user is in their journey and help them take the next step</strong>.</p>
<p>Let's go back to the workout app example:</p>
<ol>
<li>The first flow gets them to Workout 1</li>
<li>The next flow gets them to Workout 2</li>
<li>After that, it's about building consistency</li>
</ol>
<p>If someone hasn't opened the app in five days, that's a churn signal — so it’s time to follow up gently from different angles. They may not have found the right program, or there may be hesitations you can remove (equipment, time, or feedback they want to give).</p>
<p>A lot of this stage is about <strong>removing hesitations, concerns, and struggles</strong>.</p>
<h3>Days 22–30: pre-renewal reinforcement</h3>
<p>If they haven't canceled yet, this is when to reinforce the decision. Don't be the dark-UX company that hides the upcoming charge. Nothing churns someone harder than suddenly realizing they've been paying for something they forgot about. Your metrics might look better for a month, but in the long term, that's not growth.</p>
<p>Instead, think: <strong>positive reinforcement, progress reports, value summaries, and achievement highlights</strong>.</p>
<p><a href="https://www.loom.com/">Loom</a> does this well, showing how many meetings you've skipped and hours you've saved — things you could work out yourself, but probably never would.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/bda4f3c5ce9206b4e9c1592096ad75f632696ec8-1400x994.jpg" alt="screenshot showing Loom review email with information on how many meetings you saved"/></figure>
<p>I love it when a workout app tells me how many minutes I've trained or that I'm in the top 10% this month. Arbitrary numbers? Completely, but they make me feel like I've dedicated myself to something.</p>
<p>This means when the &quot;Should I renew?&quot; thought arrives, there’s already a positive feeling attached to the decision. Even better: once they've cleared that first renewal, that's your opening to <a href="https://www.revenuecat.com/blog/growth/subscription-app-expand-value">suggest a longer plan</a>, so they don't have to face this decision every single month.</p>
<p>Overall, monthly subscriptions need more touchpoints than weekly subscriptions, weighted toward the start, dwindling over time, but never stopping completely. Adjust to the user: if they're highly active, acknowledge it rather than pushing them to do more. If they're not, help them take action and figure out what's holding them back.</p>
<h2>The annual subscriber: 90 days to embed</h2>
<p>A startup recently reached out to me: great product usage, but a far bigger drop at the annual renewal than they expected. It wasn't the first time I'd heard this, and it won't be the last.</p>
<p>Annual renewals are deceptive. Cancellations spike a little in Month 1, taper off, and you start thinking: &quot;All is well with the world, they love us, they're staying&quot;. Then the first wave of renewals arrives, and it's a nasty shock.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/2fb03f52dffde0abdc28160be4591db3343075fd-1998x1418.jpg" alt="chart showing first three annual subscription renewals by app category"/></figure>
<p>Here's the thing: starting to prevent that churn at Month 11 is way too late. In my experience, most renewal decisions are made in the first 90 days, with data suggesting that <strong>around 30% of </strong><strong><a href="https://www.revenuecat.com/blog/growth/annual-subscriptions-apps-pros-cons/">annual subscribers</a></strong><strong> cancel in the first month</strong>.</p>
<p>Annual subscribers paid a large sum upfront. That makes them more motivated than weekly or monthly subscribers, which is great, but it also means <strong>they have more to lose</strong>. The gap between expectation and reality is much bigger if your product disappoints. I always say this to people who want to charge more: that's fine, but be conscious that expectations rise with the price. The data backs this up: <a href="https://www.revenuecat.com/blog/growth/average-subscription-renewal-rates-by-app-category/">low-priced annual plans see first renewal rates around 36%, while high-priced ones manage just 23%</a>.</p>
<p>A disappointed $10 subscriber shrugs, cancels, and moves on. A disappointed annual subscriber feels burned: they request refunds, they leave negative reviews. The stakes feel much higher because they made a bigger commitment upfront.</p>
<p>So <strong>focus on the first 90 days</strong>. That's where the biggest drop-off happens; where your biggest opportunity is. If an annual subscriber is still active after around 90 days, the chance they renew is dramatically higher.</p>
<p>One thing to keep in mind: if an annual subscriber cancels, <a href="https://www.revenuecat.com/blog/growth/20-of-your-churned-users-will-come-back-but-are-you-ready/">reactivation rates are extremely low, averaging around 5%.</a></p>
<h3>Month 1<strong>: </strong>activation is key</h3>
<p>This looks similar to what we covered in the monthly renewal section:</p>
<ul>
<li>First value</li>
<li>Core value</li>
<li>Reinforce value</li>
</ul>
<h3>Months 2–3: keep showing up</h3>
<p>This is where apps ghost their customers. They have flows and fancy setups for the first seven days, maybe 30, then it’s radio silence.</p>
<p>Instead, keep integrating into the user's life until using your app feels so natural <a href="https://www.revenuecat.com/blog/growth/how-subscription-apps-can-become-painkillers">they can’t imagine being without it</a>: a workflow, a daily habit, a routine. Embedding is the goal. Airship found that apps sending relevant pushes in the first 90 days see <a href="https://www.airship.com/blog/7-mobile-engagement-statistics-that-show-how-push-notifications-boost-roi/">retention around 3x higher</a> than apps sending none.</p>
<h3>Months 3–12: don't let it go stale</h3>
<p>The first 90 days matter most, but there’s a later risk: the product starts feeling same-y. Maybe they've achieved their main <a href="https://www.revenuecat.com/blog/growth/what-drives-users-to-pay-jobs-to-be-done">job-to-be-done</a> and want to be challenged differently.</p>
<p>I had this with <a href="https://www.revenuecat.com/blog/growth/peloton-retention-takeaways/">Peloton</a>: loved it for the first few months, and if you'd looked at my first 90 days, you'd have bet on me retaining it. But after a while, it felt like they weren't hearing me anymore, and I stopped making progress.</p>
<p>So keep check-ins going throughout the year: lower frequency, higher quality. Annual subscribers chose commitment; you don't need to convince them as heavily as monthly users. But you <em>do</em> need to <strong>show that the product is improving and its use case is still supported</strong>.</p>
<p>Personalized milestones are powerful here. I loved <a href="https://subclub.com/episode/how-ladder-cracked-tiktok-and-grew-500-greg-stewart-ladder">Ladder's</a> recent summer update: extra flex workouts for while you're traveling, plus new programs I could switch to if I got bored with mine. It made me feel like they understood where I was as a customer and what I needed.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/6cac6bb5e0bd9a42b27ccbc4b78b71035a208e06-1178x774.png" alt="three screenshots showing updates from Ladder app revealing their summer series campaign"/></figure>
<h2>How to find your first renewal predictor</h2>
<p>There is often a single early behavior (or a small set of behaviors), measurable in the first billing cycle, that predicts whether a subscriber makes it to renewal. This is your First Renewal Predictor (FRP).</p>
<p>Here's how to find it:</p>
<ol>
<li><strong>Map your renewal curve and find where it stabilizes: </strong>where does churn drop off dramatically? For most apps, that's around Renewal 2/3, which is what the <a href="https://www.revenuecat.com/state-of-subscription-apps/">benchmarks show across subscription types</a>.</li>
<li><strong>Compare early behaviors of survivors vs. churners:</strong> depending on plan type, look at Day 7, 14, or 30 behavior for subscribers who reached that stability point versus those who didn't.</li>
<li><strong>Identify the actions that most differentiate them:</strong> that's your FRP.</li>
</ol>
<p>For Duolingo, <a href="http://blog.duolingo.com/how-duolingo-streak-builds-habit/">the 7-Day streak is a predictor</a>: one week of consistent engagement means the chance of long-term retention jumps. A streak doesn't just mean 20 minutes a day for seven days, it means <strong>they engaged in some meaningful way each day</strong>.</p>
<h3>What first renewal predictors are <em>not</em></h3>
<p>What FRPs are typically not: session counts, onboarding completion, and trial starts. Those are <em>volume signals</em>, and volume doesn't prove usefulness. I've had days when I opened and closed a recipe app five times just trying to figure out what to cook this week, without selecting a recipe in the end. One open with a recipe chosen would be a far better signal of success than five opens with no action</p>
<p>FRPs are quality signals:</p>
<ul>
<li>A specific core action completed X times (sometimes within a date range)</li>
<li>Engagement with a feature that only high-LTV users touch</li>
<li>A returning pattern (back on Day 3 <em>and</em> Day 7, not just a single visit)</li>
</ul>
<p>When analyzing this I also look at the cohort size. It's great if someone meditates 20 times in Week 1, but if only a tiny percentage of users do that, it's not a realistic target. Depending on the engagement level overall, I look for what the top 10% are doing. Then break the core action down by frequency to find the level that <strong>balances predictive power with a cohort big enough to matter</strong>.</p>
<p>Finally, it’s worth noting your FRP may vary per subscription duration, and can be very similar (or the same) as your <a href="https://www.revenuecat.com/blog/growth/activation-metrics/">activation metric.</a></p>
<h2>If you get activation right, the first renewal takes care of itself.</h2>
<p>If first renewal predictors sound suspiciously like activation signals, you're not wrong. Most FRPs are activation metrics, measured from a different angle. With a weekly or monthly plan, you don't have time to build a full habit and relationship before the first renewal. The real work is activating the user and helping them experience the product's value. (Annual — and maybe quarterly — plans are the exception: there you have to go beyond activation and focus on embedding.)</p>
<p>So if you have a first renewal problem, don't go shopping for a retention solution. Review your activation window and fix the product experience within the first seven, 30, or 90 days, depending on your plan type. The renewal will follow.</p>
<p>This is also why win-back campaigns aimed at <a href="https://subclub.com/episode/how-to-re-engage-churned-users-caroline-walthall-quizlet">re-engaging first-renewal churned users</a> convert so badly: <a href="https://www.revenuecat.com/blog/growth/20-of-your-churned-users-will-come-back-but-are-you-ready/">those users never activated</a>, so there's nothing to win them back to.</p>
<p>I really like how Alice Muir explains it in the <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps Report 2026:</a></p>
<p><em>&quot;In a market where year-one retention is declining across all durations, the winning strategy is not gating harder or charging differently. It’s accelerating time-to-value before the first renewal decision.&quot;</em></p>
<p>Remember Greg, the plant care app? Their monthly retention sits at 21% after 12 months, well above the median. But the more interesting part is what happens next: churn nearly stops. They lose just 3% more subscribers over the following nine months. Survive the early renewals, and your subscribers don't just stay. They settle in.</p>
<p>The first renewal isn't actually where you win or lose, it's where you find out whether you already lost.</p>
<h3>How to find your first renewal rate in RevenueCat</h3>
<p>Get started reducing churn by checking your <a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/subscription-retention-chart">first renewal rate by plan type in your RevenueCat charts</a>: if it's below your category's median, the gap is almost certainly in your activation window, not your paywall, pricing, or marketing.</p>
<p>Here’s a quick step-by-step to check your first renewal rate in RevenueCat. You can also <a href="https://www.revenuecat.com/feature/ai-agent">ask Rico</a> to pull the numbers directly for you.</p>
<ol>
<li><strong>Open </strong><strong><a href="https://www.revenuecat.com/feature/charts">Charts</a></strong> in the RevenueCat dashboard (left nav) and select the <em>Subscription Retention</em> chart</li>
<li><strong>Set your date range</strong> with the range selector: this chart cohorts subscribers by their <strong>first purchase date</strong>, so pick a window old enough that the cohorts have had a chance to hit their first renewal (e.g. for monthly plans, cohorts at least ~1 month old; for annual, at least ~1 year old)</li>
<li><strong>Segment by plan type:</strong> use the segment/breakdown control and choose <em>Product</em> (or <em>Product duration</em>) so each plan gets its own retention line</li>
<li><strong>Read the </strong><em><strong>Period 1</strong></em><strong> column/point:</strong> that's the share of each cohort that made their <strong>first renewal</strong> (aka, paid a second time). Period 2, 3, 4 etc. show subsequent renewals</li>
</ol>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/d13fd09a25a49969e5520c3268975e2e556dfdd1-2008x1664.png" alt="screenshot showing Rico conversation in Slack sharing first renewal rate for an app"/><figcaption>Rico's answer via Slack when asked about the first renewal rate for the app we acquired to test new feature sand functionality</figcaption></figure>
<p>Two things to watch:</p>
<ul>
<li>When you segment by a dimension other than the <em>subscription start cohort</em>, subscribers in a segment can have different start dates — and therefore different renewal opportunities. The chart only measures the portion of the cohort that has actually had the chance to renew in that period, so don't compare a period's rate against the full cohort size.</li>
<li>Duration = period length: <em>Period 1</em> means one billing cycle later (one month for monthly plans, one year for annual plans etc.). That's why segmenting by plan type is the right move — it keeps the comparison apples-to-apples.</li>
</ul>
<p>
</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Third-party Android app stores: what the current setup looks like for US developers]]></title>
      <link>https://www.revenuecat.com/blog/growth/third-party-android-app-stores-us</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/third-party-android-app-stores-us</guid>
      <pubDate>Mon, 31 Aug 2026 06:57:47 GMT</pubDate>
      <dc:creator><![CDATA[Margarita Loktionova]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Third-party Android app stores are getting easier to access in the US. Here’s how the current setup works, which stores matter, and what developers need to know about sharing their Google Play listings.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/2b35af9ba527218f14df5c73116894f632331365-2400x1275.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Third-party app stores have been part of Android from the start. Some come preinstalled, like Galaxy Store on Samsung phones or Amazon's Appstore on Fire devices. Installing most other stores in the US typically meant sideloading: digging through security settings, downloading an APK from the web, and clicking past warnings.</p>
<p>For some stores, that's no longer the case.</p>
<p>Under changes required by the Epic v. Google ruling, approved third-party stores in the US can be installed from inside Google Play and can offer apps from Play’s own catalog. Aptoide Games is already enrolled, and more will likely follow.</p>
<p>In this guide, we'll cover what third-party Android stores are, how the program works, which stores to consider, and how to make that opt-out call on purpose.</p>
<aside class="tip"><strong>TL; DR.</strong><p>The July 22, 2026 changes affect how apps get discovered and installed via third‑party storefronts in the US. But they don’t change what you earn: downloads still complete through Google Play, and Google’s fee still applies. One aspect directly affects Android developers today: your app’s listing is being shared with these stores by default, unless you’ve opted out in the Play Console.</p></aside>
<h2>What are third-party Android app stores?</h2>
<p>Third-party Android app stores are app marketplaces that operate outside Google Play. Each one has its own catalog, its own search, and its own update mechanism.</p>
<p>The main ones operating in the US include:</p>
<ul>
<li><strong>Amazon Appstore:</strong> Amazon's store for Fire tablets and Fire TV devices, with its own billing system.</li>
<li><strong>Samsung Galaxy Store:</strong> Preinstalled on every Samsung phone, with its own billing and subscription system.</li>
<li><strong>Epic Games Store:</strong> Epic's own storefront, and the company whose lawsuit against Google set this year's changes in motion.</li>
<li><strong>Aptoide:</strong> Portugal-based, with around 25 million monthly users, and the first store to appear inside Google Play in the US under this program.</li>
<li><strong>F-Droid:</strong> A catalog of open-source apps only, with no billing system at all.</li>
<li><strong>Outside the US</strong>: The list gets much longer, with ONE Store in South Korea, Xiaomi's GetApps, and dozens more.</li>
</ul>
<p>The difference has always been in distribution. Google Play ships on nearly every Android device. Galaxy Store comes preinstalled on Samsung phones, and Amazon Appstore on Fire devices. Most other independent stores, however, faced the same two hurdles: users had to sideload them, and their catalogs had to be built from scratch.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ed2e861e90cdf69bff83ad84c6d1fd9fd0251bfd-2880x1620.png" alt="How apps reach Android devices - scheme"/></figure>
<p>The Play Catalog Access Program tackles these limitations.</p>
<h2>The Play Catalog Access Program</h2>
<p>The <a href="https://support.google.com/googleplay/android-developer/answer/17117200?hl=en-GB">Play Catalog Access Program</a> lets approved third-party stores in the US access Google Play's app catalog and offer those apps to users. Downloads still complete through Google Play, and Google's fee still applies.</p>
<p>This solves both problems we covered above: enrolled stores can be installed from inside Google Play, and they can offer apps from Play's catalog instead of building their own from scratch.</p>
<p>A few things to know about how it works:</p>
<ul>
<li><strong>It comes from a court order.</strong> Google created the program to satisfy the permanent injunction in Epic v. Google. The injunction was entered in October 2024 and became effective on November 1, 2024, and the catalog-access provisions took effect on July 22, 2026.</li>
<li><strong>It won't last forever, at least not automatically.</strong> The court order's remedies run for three years from the November 1, 2024 effective date, which puts the end around November 1, 2027. Even though some pieces, like catalog access, only arrived in mid-2026.</li>
<li><strong>Enrollment is gated for app stores</strong>. They have to meet Google's security and policy requirements and pay $5,000 upfront, plus $5,000 a year.</li>
<li><strong>Google Play's full content policies don't apply</strong>. Stores set their own rules, subject to Google’s eligibility and safety requirements.</li>
</ul>
<h2>Installing a third-party store: before vs. now</h2>
<p>Until July, getting an independent store onto a user's phone was a small act of determination.</p>
<p>The user went to a website, downloaded an APK, and Android did everything short of begging them to stop: a settings toggle, a warning screen, a blocked install.</p>
<p>The new system is supposed to make it as simple as installing any other app:</p>
<table>
<thead><tr>
<th></th>
<th><p><strong>Before the program</strong></p></th>
<th><p><strong>Now</strong></p></th>
</tr></thead>
<tbody>
<tr>
<td><p><strong>How users find these stores</strong></p></td>
<td><p>Web search, manual APK download</p></td>
<td><p>Search inside Google Play</p></td>
</tr>
<tr>
<td><p><strong>How users install them</strong></p></td>
<td><p>&quot;Unknown sources&quot; toggle, warning screens</p></td>
<td><p>Directly from Google Play, with some extra steps still being removed</p></td>
</tr>
<tr>
<td><p><strong>Where the store's catalog comes from</strong></p></td>
<td><p>Built from scratch</p></td>
<td><p>Can include Google Play's own listings</p></td>
</tr>
<tr>
<td><p><strong>How your app's downloads complete</strong></p></td>
<td><p>Through the store's own systems</p></td>
<td><p>Through Google Play</p></td>
</tr>
<tr>
<td><p><strong>Whose content policies cover your listing</strong></p></td>
<td><p>The store's own</p></td>
<td><p>The store's own, unchanged</p></td>
</tr>
<tr>
<td><p><strong>What users pay for these installs</strong></p></td>
<td><p>No such installs existed</p></td>
<td><p>Nothing. These installs run through Google Play's billing.</p></td>
</tr>
</tbody>
</table>
<p>In practice, the first weeks were rockier. Rival stores were hard to find in Google Play's search, and installing one took extra steps. At an August 13 hearing, Judge James Donato <a href="https://www.androidpolice.com/expect-easier-access-to-alternative-android-app-stores-next-week/">ordered </a>Google to fix this within a week, and Google agreed to make the changes by August 20.</p>
<h2>What this means for developers</h2>
<p>The Play Catalog Access Program doesn't require any code tweaks or affect what you pay Google. What matters is one page in Play Console, called Catalog Settings: since July 22, it's been sharing your listing with enrolled third-party stores by default.</p>
<p>In <a href="https://play.google.com/console/">Play Console</a>, go to <strong>Settings</strong>, then <strong>Catalog Settings</strong>. You'll see <strong>three options</strong>:</p>
<ul>
<li>Share all listings with all enrolled stores</li>
<li>Manage per store</li>
<li>Opt out</li>
</ul>
<p>The first option is preselected, so unless someone on your team has changed it, sharing is already on. It doesn't add extra cost and increases visibility. The catch is that every enrolled store sets its own content policies. Google doesn't control what else gets listed there or how your app is displayed.</p>
<p>Whether that matters depends on your app. </p>
<p>If you have compliance obligations or an audience that needs extra protection (e.g., healthcare, finance, apps for kids), switch to the per-store option and vet each store before allowing it. Read its trust and safety policies, browse its catalog, and see how it presents apps like yours. Then, allow the ones that pass, skip the ones that don't, or opt out of the program entirely.</p>
<aside class="tip"><strong>For now, the review is short</strong><p>Aptoide Games is the only enrolled store visible in Google Play, but more will likely join, so check back as the list grows.</p></aside>
<h2>Where to go from here</h2>
<p>The Catalog Access Program adds enrolled stores like Aptoide Games to that list, making reaching new users much more accessible.</p>
<p>A few other things to keep in mind:</p>
<ul>
<li><strong>Aptoide Games is the only enrolled store visible so far</strong>, so revisit your choice as more stores join.</li>
<li><strong>Your fees and billing stay the same no matter what you pick</strong>. Google's other programs, like alternative billing (letting users pay through a non-Google payment system) and external offers (linking users out to buy on your website), are a <a href="https://www.revenuecat.com/blog/engineering/play-billing-choice">separate decision</a>.</li>
<li><strong>Existing Amazon Appstore integrations on Fire devices and Galaxy Store integrations aren't affected</strong>, so there’s nothing to change there. If you distribute through <a href="https://www.revenuecat.com/docs/platform-resources/amazon-platform-resources">Amazon Appstore</a> or <a href="https://www.revenuecat.com/docs/platform-resources/galaxy-platform-resources/galaxy-setup-guide">Galaxy Store</a>, and don't want to manage each store's billing yourself, RevenueCat's SDK supports both.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Short drama has cracked mobile acquisition and early monetization]]></title>
      <link>https://www.revenuecat.com/blog/growth/short-drama-mobile-acquisition-monetization</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/short-drama-mobile-acquisition-monetization</guid>
      <pubDate>Fri, 28 Aug 2026 09:07:49 GMT</pubDate>
      <dc:creator><![CDATA[Rik Haandrikman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[ Here’s what the rest of streaming can learn]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/aae908fdff252d10cc2c0d5dc3e87af4249525b1-2400x1275.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Remember Quibi? It may be one of the most expensive examples of a company arriving just a little early</p>
<p>For those who weren’t online yet: in 2020, Quibi bet that people would watch premium stories made for their phones, delivered in episodes of ten minutes or less. It <a href="https://www.latimes.com/entertainment-arts/business/story/2020-10-21/quibi-shutting-down-after-subscriber-struggles">raised an eye-watering $1.75 billion and was shuttered less than a year after going live</a>. And with that, short-form video for mobile died and was never seen or heard from again. Or did it?</p>
<p>Today’s short-drama apps were built for how people actually use phones: upright, in short bursts, and often one cliffhanger away from paying. They combine serialized stories, aggressive paid acquisition, and monetization that begins almost immediately. This time, the numbers look a little different</p>
<h2>China got there first</h2>
<p>Quibi approached mobile video from the top down. It brought Hollywood budgets, stars, production practices, and a conventional streaming subscription to a smaller screen. The episodes were shorter, but much of the machinery still came from television</p>
<p>Short drama as we see it in the app stores today developed in China. Its immediate roots lie in the serialized shorts that spread across platforms such as <a href="https://www.tencent.com/en-us/articles/2201874.html">Douyin and Kuaishou</a> in the late 2010s. <a href="https://www.nrta.gov.cn/art/2022/12/27/art_110_63202.html">Chinese regulators</a> <a href="https://www.nrta.gov.cn/art/2023/12/24/art_3731_66456.html">formally recognized micro-short drama as a distinct format in 2020</a>, and the market accelerated during the pandemic. It originates in an ecosystem where vertical video, web fiction, mobile payments, performance marketing, and fast, inexpensive production were already the norm</p>
<p>The stories were built differently too. Chinese web fiction had spent years refining plots around base human desires and conflicts: romance, revenge, sudden wealth, family betrayal, hidden identities, and dramatic reversals. Short drama translated that rhythm into video. Every episode had a job: establish the stakes quickly, resolve just enough, and create the next reason to keep watching</p>
<p>The business model developed alongside the format. Viewers could sample a story for free, pay to unlock the next episodes, watch ads to earn access, or subscribe. Distribution often started with a dramatic clip on a social platform and ended inside a mini-program or dedicated app. The same story attracted the viewer, kept them watching, and eventually asked them to pay</p>
<p>By June 2024, China had 576 million micro-drama viewers (more than half of the country’s internet users!) according to the <a href="https://www.cnnic.com.cn/IDR/ReportDownloads/202411/P020241101318428715781.pdf">China Internet Network Information Center</a>. <a href="https://gdj.beijing.gov.cn/zwxx/gzbg1/202507/t20250707_4142995.html">China’s domestic micro-drama market reached roughly RMB 50 billion that year, overtaking the country’s cinema box office</a> for the first time</p>
<p>This was already a mass-market business before most Western entertainment executives had heard of ReelShort or DramaBox. <a href="https://omdia.tech.informa.com/pr/2025/oct/microdramas-to-generate-11-billion-dollars-in-global-revenues-in-2025-says-omdia">Omdia</a> estimates that microdramas generated roughly $11 billion worldwide in 2025, with China accounting for about 83% of the total</p>
<p>What travelled overseas was more than the shows themselves. Apps such as ReelShort began producing local stories with English-speaking actors and familiar settings while keeping the rapid pacing, performance marketing, and payment mechanics that had already been tested in China</p>
<p>ReelShort founder Joey Jia told <a href="https://time.com/7173765/reelshort-crazy-map-studio-joey-jia-interview/">TIME</a> that the company can take a story from idea to live in front of an audience in roughly three months, see exactly where viewers stop watching, and rebuild a failed concept with different characters. That feedback loop is hard to reproduce when a traditional production takes years and most of its budget is committed before the audience sees a frame</p>
<h2>This is no longer contained to China</h2>
<p>Based on <a href="https://appfigures.com/">Appfigures</a> data, we analyzed over 5,000 of the highest grossing non-game Entertainment apps, and found roughly 360 short-drama apps in the listing (around 6.5% of all apps in the category). Together, they generated about 145 million estimated downloads and $230 million in estimated store revenue in the latest 30 days</p>
<p>When we look at download numbers, it becomes clear that something’s happening. At just 6.5% of the Entertainment listings, short-dramas generated <strong>38% of downloads and 13% of store revenue</strong>. Its share of downloads was nearly six times larger than its share of apps</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/833246181a01b7228d130cdd77399c2740bf9d1c-1920x1080.png" alt="Share of analysis non-game Entertainment total (%)"/></figure>
<p>Like most categories, short-drama is top-heavy. The ten largest apps generated roughly 78% of the group’s estimated store revenue. At the other end, the long tail represented about 70% of apps, attracted 23% of downloads, but only 1.5% of revenue</p>
<p>Small apps can still attract installs. Turning those installs into substantial store revenue is much harder</p>
<p>The two app stores also play very different roles. Android produced 76% of estimated downloads, while iOS produced 58% of store revenue. Android is where these apps find much of their reach; iOS is where much of the direct revenue is generated, though this explicitly ignores ad revenue. A relevant distinction to make, because over ⅔ of short-drama apps include ad monetization, compared to 37% of the overall Entertainment category</p>
<h2>What happens after the download</h2>
<p>The download is just the start. What matters next is whether that new user pays</p>
<p>We compared the short-drama apps in RevenueCat’s data with the broader Entertainment category. The short version: short drama gets people to pay much (much) faster than traditional Entertainment apps do</p>
<p>For the median short-drama app, 5.5% of installs became paying customers within 35 days. The median for other Entertainment apps was 1.6%</p>
<p>The same advantage appears in early revenue. Short-drama apps generated a median of $0.83 per install in the first 14 days, compared with $0.15 for other Entertainment apps. <strong>First-month realized revenue per paying customer was $23.85, versus $13.09</strong></p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/cbbc370ce4fa16389fc64b4883f6632c43cb92cc-1920x1080.png" alt="Short drama apps monetize installs earlier"/></figure>
<p>That’s not (just) a matter of a better paywall. Short-drama apps design the ad, episode, price, and payment moment together to form a cohesive, compelling journey</p>
<p>A typical ad does not introduce the brand and ask viewers to remember it. It drops them into a betrayal, a wedding, an inheritance, or a billionaire-shaped problem. The viewer watches long enough to need the resolution, installs the app, gets several episodes free, and reaches a <a href="https://www.revenuecat.com/feature/paywalls/personalization">highly personalized paywall</a> at the moment of maximum curiosity</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1baf935007a350d46d4db84c2ebe5451c01ee7cb-1920x1080.png" alt="The paywall matches what you were watching"/><figcaption>Screenshots taken from the app My Drama</figcaption></figure>
<p>At MIP London, executives told the <a href="https://apnews.com/article/microdrama-issa-rae-kevin-hart-hollywood-087b042a6eac08e159410e29689606b9">Associated Press</a> that some of the largest microdrama platforms spend as much as 90% of their budgets on marketing. That sounds extreme through a television lens. It makes more sense in the world of <a href="https://www.revenuecat.com/docs/integrations/attribution">mobile attribution</a> where each piece of creative can be tied to an install, an episode, a payment, and eventually a renewal</p>
<h2>This looks more like a mobile game than Netflix</h2>
<p>Short-drama apps do not rely on a single monthly subscription. They borrow freely from mobile games: virtual coins, consumable purchases, rewarded ads, limited-time offers, and short subscription periods all sit next to one another</p>
<p>That mixed model is visible in the Appfigures data. Store metadata showed in-app purchases on 93% of short-drama listings, subscriptions on 86%, and advertising on 67%. More than half showed all three, and those apps generated roughly 93% of the short-drama store revenue in our sample</p>
<p>It’s a great example of how apps use <a href="https://www.revenuecat.com/blog/growth/hybrid-monetization-techniques">mixed monetization</a> to ‘drop the floor’ (make it possible for the masses to generate a little revenue) and ‘raise the ceiling’ (allow highly engaged users to pay again and again)</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1c9f35eabb7aa6eaec786ce438fd61ac5cee6fa9-1920x1080.png" alt="Ads, subscription and coins - on one screen"/><figcaption>Taken from the app Netshort</figcaption></figure>
<p>Our own data shows just how strongly the subscription side is built around urgency. For the median short-drama app, <strong>weekly plans accounted for 88.5% of recurring revenue</strong>, compared with 14.3% for other Entertainment apps. The most common weekly price was $9.99 - about twice the $4.99 that the rest of Entertainment usually charges</p>
<p>In this case, weekly pricing fits. A viewer can discover a series, binge dozens of minute-long episodes, and make a relatively expensive purchase in a single session while also actually experiencing the full value in that same session. The app does not need to convince that person it deserves a permanent place alongside Netflix. It needs to make the next episode feel worth paying for now</p>
<h2>Then comes renewal</h2>
<p>The first payment comes quickly. The second is harder</p>
<p>The median first-renewal rate for short-drama apps was 31.1%, compared with 44.3% for other Entertainment apps. In the smaller group for which we could measure six-month retention, the gap was wider: 1.8% for short drama versus 8% for the rest of Entertainment</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/aad89a1966d39b085ef206161c188e3be263f159-1920x1080.png" alt="Early monetization does not translate into stronger retention"/></figure>
<p>The model works brilliantly in the moment. In our data, it is much less effective once that moment has passed. These numbers do not tell us whether that comes from story completion, weekly pricing, acquisition mix, or something else - but they make the trade-off difficult to ignore</p>
<p>The opportunity is fairly obvious: bring short drama’s mobile speed together with the libraries, franchises, and subscriber relationships traditional media already knows how to build</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/dbdcd283a1283ca96d017a828e7ca1d33b2a9bb3-1920x1080.png" alt="Short drama comes to the Lock Screen"/><figcaption>Short drama has embraced reengagement tactics like lockscreen activities, shown here with DramaBox, My Drama, and GoodShort</figcaption></figure>
<h2>The rest of streaming is paying attention</h2>
<p>Traditional media has moved from watching this category to investing in it</p>
<p>In the US, <a href="https://omdia.tech.informa.com/pr/2026/feb/microdramas-overtake-streamers-on-mobile-engagement-says-omdia">Omdia</a> found that ReelShort users spent an average of 35.7 minutes per day in the app in late 2025, compared with 24.8 minutes for Netflix on mobile. ReelShort’s audience was much smaller, but those viewers spent considerably longer in the app each day</p>
<p>The industry moves are becoming concrete. <a href="https://thewaltdisneycompany.com/news/disney-accelerator-companies-2025/">Disney</a> selected DramaBox for its 2025 Accelerator program. <a href="https://www.foxcorporation.com/news/business/2025/fox-entertainment-deepens-creative-content-portfolio-and-audience-reach-with-strategic-investment-in-vertical-video-technology-platform-holywater/">Fox</a> took an equity stake in Holywater, the company behind My Drama, and committed to producing more than 200 vertical titles over two years. <a href="https://www.nbcuniversal.com/article/peacock-deepens-mobile-engagement-vertical-video-and-original-microdramas">Peacock has launched a dedicated microdrama hub</a>, TelevisaUnivision is producing vertical series for ViX, while TikTok is now testing a standalone app called LimeShorts, designed specifically for short vertical series</p>
<p>Not everyone needs to copy these billion dollar investments, or start cropping every show into portrait mode. The lessons that are up for grabs for anyone are on how tightly short-drama companies connect marketing, viewing, payment, and production. They can see which ad brought someone in, where that viewer stopped, what persuaded them to pay, and whether they returned</p>
<p>The weak point is retention. Established media companies already have the libraries, franchises, and programming experience that could help solve it</p>
<p>Quibi may have been early, but timing was not the only difference. The companies that followed rebuilt the idea around the phone - and found a business model that worked</p>
<h2>Let us help!</h2>
<p>Building a vertical-video product or rethinking mobile growth? <a href="https://www.revenuecat.com/events/ibc-2026">Come talk to RevenueCat at IBC 2026 in Amsterdam this September!</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to submit your app for Shipaton]]></title>
      <link>https://www.revenuecat.com/blog/engineering/how-to-submit-your-app-for-shipaton</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/how-to-submit-your-app-for-shipaton</guid>
      <pubDate>Thu, 27 Aug 2026 12:23:38 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[A detailed walkthrough of the five Devpost steps, what to prepare, and how to make sure your app gets judged
]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/c60e0ab9bd2840773739b2fa79b4c18d8b6374b2-1600x818.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>How to submit your app for Shipaton</p>
<p>A detailed walkthrough of the five Devpost steps, what to prepare, and how to make sure your app gets judged</p>
<p>Your app is built, RevenueCat is integrated, and the store review is behind you.</p>
<p>There’s one job left: submitting it to Shipaton. Or maybe you’re not yet there, since there are still days left to submit. No matter what the case, you should start looking at how to submit your app to Shipaton at this point.</p>
<p>Shipaton submissions are sent through <a href="https://revenuecat-shipaton-2026.devpost.com/">Devpost</a>. The submission form has five sections: Manage team, Project overview, Project details, Additional info, and Submit. Here’s how to get through each step without a headache.</p>
<p>The submission deadline is Sep 30, 2026 at 11:45 pm PDT. You can save a draft and edit a submitted project until the deadline, but don’t leave the final click until the final minute.</p>
<h2>First, make sure your app is eligible</h2>
<p><strong>For the main Shipaton competition </strong>(all other categories than Next Gen), your app must:</p>
<ul>
<li>Be an iOS, iPadOS, macOS, or Android app; web apps are not eligible.</li>
<li>Use the RevenueCat SDK to power at least one in-app or web purchase, or serve ads through RevenueCat Ads.</li>
<li>Be a new app whose first public version was released during the Shipaton submission window.</li>
<li>Be fully published on the Apple App Store, Google Play Store, Mac App Store, or Samsung Galaxy Store.</li>
<li>Work as shown in your submission video and description.</li>
<li>Be available to download in the United States.</li>
</ul>
<p>Apps released before Shipaton don’t qualify, and moving an existing app to another store doesn’t make it new. An existing web app can qualify if your Shipaton submission is an iOS, iPadOS, macOS, or Android version of that app that users can download from the mentioned App Stores.</p>
<p>TestFlight or testing-track builds don’t count. Shipaton needs a live store listing, and app review can take days, so don’t leave it until deadline week.</p>
<p>Students entering the Next Gen Award get an exception: they can submit a demo video and public open-source repository instead of a store listing. You can enter both the Next Gen Award and any of the other categories by providing a public open-source repository, demo video, and an app store link.</p>
<h2>Prepare these materials for your submission</h2>
<p>Get these assets ready for your submission. All of them are required, and missing any of them will make your app not eligible for judging:</p>
<ul>
<li>Your project name and a short tagline.</li>
<li>A description of the app and its main features.</li>
<li>A public App Store, Google Play, Mac App Store, or Galaxy Store URL.</li>
<li>A public YouTube or Vimeo demo video.</li>
<li>A 1024 × 1024 app icon.</li>
<li>RevenueCat project ID</li>
<li>At least one 1179 × 2556 app screenshot without a device frame.</li>
<li>A free trial or promo code that lets judges test all premium features.</li>
<li>Any category-specific links, IDs, metrics, or explanations.</li>
</ul>
<p>Some sponsor categories might have extra requirements; see official rules for requirements on those. Everything you submit needs to be in English.</p>
<h2>Filling the Devpost submission</h2>
<p>Register for Shipaton, start a project, and save it as a draft. Devpost splits the submission into five steps.</p>
<h3>Step 1: Manage your team</h3>
<p>Start by adding everyone who worked on the app.</p>
<p>Solo project? You’re done here. For a team project, invite people with the email tied to their Devpost account or send them Devpost’s private invitation link. Add everyone before the deadline.</p>
<h3>Step 2: Complete the project overview</h3>
<p>The project overview is the card people will see in the Devpost gallery. It asks for:</p>
<ul>
<li><strong>Project name:</strong> Use the same recognizable name as your store listing.</li>
<li><strong>Tagline:</strong> Explain what the app does in one short sentence.</li>
<li><strong>Thumbnail:</strong> Upload a clean image that remains readable at a small size. Devpost recommends a 3:2 image in JPG, PNG, or GIF format.</li>
</ul>
<p>Here's my tip on how to do this well: skip taglines like “An AI-powered productivity platform.” Say what the product actually does: “Practice difficult conversations before having them with your team.”</p>
<h3>Step 3: Tell your project’s story</h3>
<p>Project details are the core of the submission: what you built, why it matters, and a working demo. <a href="https://help.devpost.com/article/126-know-your-submission-steps">Devpost’s submission guide</a> divides this into the project story, technology tags, links, images, and demo video. Let’s go through each part in detail:</p>
<p><strong>Write a description judges can scan</strong></p>
<p>Answer the basics first:</p>
<ol>
<li>What problem does it solve?</li>
<li>Who is it for?</li>
<li>What does it let them do?</li>
<li>How does it make money?</li>
<li>What makes it interesting or different?</li>
<li>Which award categories are you targeting?</li>
</ol>
<p>Add the build story: what was hard, what you learned, and what comes next. Break it up with headings and short paragraphs. You can use AI to correct spelling and formatting, but <strong>don’t let AI write your whole description. No one will like reading that.</strong></p>
<p>Include the proof each category asks for. Make the job for judges easier by explaining why your app qualifies for selected categories, and what makes it stand out.</p>
<p><strong>Add your technology tags</strong></p>
<p>Tag RevenueCat and the main tools you used. Devpost allows up to 25 tags. If you’re entering a sponsor category, tag that product too: Kotlin Multiplatform, Replit, OneSignal, Layers, Stripe, or whichever one applies.</p>
<p>These are mainly used for explaining your project; they don’t affect judging.</p>
<p><strong>Add the public app link</strong></p>
<p>For a standard entry, link to the fully published app on:</p>
<ul>
<li>Apple’s App Store or Mac App Store.</li>
<li>Google Play.</li>
<li>Samsung Galaxy Store.</li>
</ul>
<p>Test the store link in a private browser window. The app needs to be downloadable in the US.</p>
<p>The store-link exception applies only to Next Gen. In any other case, <strong>you need to have a store link in your submission to be considered eligible</strong>.</p>
<p><strong>Upload your images</strong></p>
<p>For Shipaton, that means:</p>
<ul>
<li>A 1024 × 1024 app icon.</li>
<li>At least one screenshot at 1179 × 2556.</li>
<li>Screenshots without device frames.</li>
</ul>
<p>Pick screenshots that tell the story. Five near-identical screens won’t help; a good set should show the core experience, the monetization, and the product’s strongest visual details. If you end up winning we will use the app icon and one of the screenshots in the marketing materials.</p>
<p><strong>Add your demo video</strong></p>
<p>Upload the video to YouTube or Vimeo. Unlisted YouTube videos are fine; private ones won’t be viewable by the judges, so don’t do that.</p>
<p>Keep the essential demo under two minutes, as <strong>judges aren’t required to watch beyond the 2 minutes. </strong>Show the app running on the device it was built for, and highlight the most important features and aspects of your app.</p>
<p>In the first two minutes, cover:</p>
<ul>
<li>Your app’s elevator pitch.</li>
<li>The app’s core experience in use.</li>
<li>Its purchase, subscription, web-payment, or advertising experience.</li>
<li>The prize categories you are targeting and why the app fits them.</li>
</ul>
<p>Don’t spend the week making a trailer for your app or game. A clear screen recording with concise narration beats a long intro or elaborate title sequence.</p>
<p>As explained in <a href="https://www.shipaton.com/blog/how-we-judge-shipaton">How we judge Shipaton</a>, judges make most of their decisions from the description, video, and screenshots. Lead with the stuff that makes your app stand out, and mention which categories you are targeting.</p>
<p>If you haven’t made one before, keep it simple: record the screen, add a clear voice-over, and tell one coherent story. Our <a href="https://www.revenuecat.com/blog/engineering/how-to-win-shipaton-part-4-pitching-your-app">guide to storytelling</a> can help you structure the pitch. Avoid copyrighted music, third-party trademarks, and other protected material unless you have permission to use them. <strong>For the influencer categories, do not use the likeness or brand of the influencers.</strong></p>
<h2>Step 4: Choose your categories and complete the additional information</h2>
<p>Additional info holds the Shipaton-specific fields: media, store links, your RevenueCat project ID, and any promo code judges need for premium features.</p>
<p>You can enter several compatible categories, but don’t tick boxes for the sake of it. Each one may need its own explanation, link, account ID, or proof.</p>
<p>The <a href="https://revenuecat-shipaton-2026.devpost.com/rules">official Shipaton rules</a> contain the complete category requirements and judging criteria.</p>
<h3>RevenueCat awards</h3>
<ul>
<li><strong>Grand Prize: </strong>Share what happened after launch, using the numbers that best make your case: downloads, revenue, active users, paid customers, conversion, retention, or something else.</li>
<li><strong>HAMM Award</strong>: Explain the business model, pricing, paywall, conversion, and revenue, including what you changed or learned.</li>
<li><strong>Catvertising Award</strong>: Show where RevenueCat Ads appear, why those placements fit the experience, and how ads work alongside other revenue.</li>
<li><strong>RevenueCat Design Award</strong>: For exceptional craft, interaction design and animation. Point judges to the details worth noticing.</li>
<li><strong>RevenueCat Peace Prize</strong>: For apps with a positive impact on people, communities or society. Be specific about who benefits and what changes for them.</li>
<li><strong>Best Game Award</strong>: For the strongest mobile game. Cover the gameplay loop, progression or replayability, art direction and why the monetization belongs.</li>
<li><strong>#BuildInPublic Award</strong>: For the best public build story. Link to your posts and show how feedback, accountability or the community changed the app.</li>
<li><strong>Next Gen Award</strong>: For qualifying student projects. Use an academic email and submit a demo plus a public repository with code, setup instructions and a visible open-source license.</li>
<li><strong>Conflict of Interest Award</strong>: Only for eligible RevenueCat and sponsor employees.</li>
</ul>
<p>All eligible projects can be considered for the Grand Prize, but you still need to show what happened after launch. Revenue made by the app is used for the shortlist, but it does not decide the winner. Making the most money will not automatically make you the winner of the Grand Prize award.</p>
<h3>Influencer Awards</h3>
<p>The five briefs are:</p>
<ul>
<li><strong>Productivity — Christopher Lawley:</strong> A focused tool for Apple power users to save and retrieve reusable text, documents, files, and images.</li>
<li><strong>Nutrition and Healthy Eating — Abbey’s Kitchen:</strong> A flexible nutrition app inspired by the Hunger Crushing Combo framework, without calorie counting or restrictive meal plans.</li>
<li><strong>Yoga and Fitness — Simone Sharice:</strong> A personalized companion that turns movement, Pilates, recovery, and wellness needs into a clear daily plan.</li>
<li><strong>Career Coaching — Leadership Heather:</strong> A practice environment for new managers preparing for difficult workplace conversations.</li>
<li><strong>Gaming — Mr Lewis Blogs Gaming:</strong> A gaming bucket-list app for saving, organizing, completing, rating, and sharing games.</li>
</ul>
<p>You can enter one Influencer Award per project and still enter other eligible categories. State which brief you chose and how the app fits that audience in the Devpost submission.</p>
<p>Building for an influencer’s audience doesn’t give you permission to use their name, image, voice, logo or likeness. Get express written consent before using any of it in the product, store listing or marketing.</p>
<h3>Sponsor awards</h3>
<ul>
<li><strong>Ship Kotlin Everywhere — JetBrains:</strong> Publish for both iOS and Android using Kotlin Multiplatform, Compose Multiplatform, or both. Include both store links and explain the implementation.</li>
<li><strong>Most Viral App — Noise:</strong> Use Noise to promote the live app during Shipaton. Include the app URL and the email associated with your Noise account.</li>
<li><strong>Best App for Galaxy — Samsung:</strong> Publish on the Galaxy Store and explain Galaxy-specific work such as foldable support, Samsung features, or device optimization.</li>
<li><strong>Idea to Income — Replit:</strong> Build and publish using Replit and RevenueCat through Replit Agent. Include at least three public build posts, the Replit preview URL, username, and an explanation of Replit’s role.</li>
<li><strong>Keep Them Coming Back — OneSignal:</strong> Integrate OneSignal and deploy at least one campaign. Include the OneSignal App ID and describe the messaging experience.</li>
<li><strong>The Growth Loop Award — Layers:</strong> Install the Layers SDK and describe the audience, hypothesis, experiment, observed signal, lessons, and next step.</li>
<li><strong>Funnel Vision Award — Stripe:</strong> Launch a live web-to-app funnel using RevenueCat Funnels with Stripe. Include the funnel URL and Stripe Project ID.</li>
</ul>
<p>Only enter a sponsor category once the integration works, and you have every requested detail. A technology tag on its own won’t qualify.</p>
<h2>Step 5: Review and submit</h2>
<p>The last screen is a short rules-and-terms confirmation.</p>
<p>Before you press Submit project, check:</p>
<ul>
<li>Is the store link live and available in the US?</li>
<li>Does the app install and behave like the version in your video?</li>
<li>Is the video public, and is the essential footage under two minutes?</li>
<li>Have you uploaded the correctly sized icon and screenshot?</li>
<li>Can judges access premium functionality through a free trial or promo code?</li>
<li>Have you answered the additional questions for every selected category?</li>
<li>Are your teammate details correct?</li>
<li>Are your written materials and testing instructions in English, or accompanied by an English translation?</li>
</ul>
<p>After submitting, click View and inspect the public entry. Check the formatting, embedded video, gallery, links, and category answers.</p>
<p>You’re only done when Devpost shows Submitted and 5/5 steps done. A saved draft doesn’t count.</p>
<h2>You can still edit after submitting</h2>
<p>Submitting early doesn’t lock the entry. You can make changes until the deadline. After that, edits to your Devpost portfolio won’t change the version entered into Shipaton.</p>
<p>That extra time is useful for:</p>
<ul>
<li>Catch private or broken video links.</li>
<li>Fix missing fields.</li>
<li>Improve your description.</li>
<li>Replace weak screenshots.</li>
<li>Add post-launch growth results.</li>
<li>Confirm that the store listing and promo code work.</li>
</ul>
<p>You can submit more than one app, but each entry must be unique and substantially different. When yours is ready, submit it and click View for one last look at the public entry.</p>
<h2>Frequently asked questions</h2>
<p>Still unsure whether your app qualifies? Here are the answers to some of the most common submission questions. See also our <a href="https://www.shipaton.com/faq">frequently asked questions section on shipaton.com</a> if your questions are not answered here.</p>
<h3>Can I submit more than one app?</h3>
<p>Yes. Each app must be a separate, eligible project whose first store release happens during the Shipaton window, from August 1 to September 30, 2026.</p>
<h3>Does releasing an existing app on a second store count as a new launch?</h3>
<p>No. If the app was already available in one app store before August 1, releasing it in another store during Shipaton does not make it a new app.</p>
<p>There is an exception for web apps. A project that previously existed only on the web can qualify if its first native app-store release happens during the Shipaton window.</p>
<h3>What if my app is still waiting for store review at the deadline?</h3>
<p>It will not qualify. Judges need to be able to download the published app from a supported store.</p>
<p>Store review can take several business days—and longer if the build is rejected—so RevenueCat recommends submitting it for review at least one week before the Shipaton deadline.</p>
<h3>Can I participate without an Apple or Google developer account?</h3>
<p>Possibly, through the Next Gen Award. Eligible students can submit a demo and public open-source repository instead of publishing the app to a store.</p>
<p>You will need a verifiable academic email address from a school, university, bootcamp, or another academic program. Standard Shipaton categories still require a published app and working RevenueCat integration.</p>
<h3>Can I enter the Next Gen Award and another category?</h3>
<p>Yes, provided you meet both sets of requirements. That means submitting the code required for Next Gen and publishing the app to a supported store for the other category.</p>
<p>The store release is not considered when judging the Next Gen Award itself.</p>
<h3>Does every submission need to use RevenueCat?</h3>
<p>Yes. Standard entries must use the RevenueCat SDK for purchases, subscriptions, or RevenueCat Ads.</p>
<h3>Is there a limit to the number of teammates?</h3>
<p>No. Your team can have any number of members. However, if an eligible prize includes travel to New York, RevenueCat will fly one team member.</p>
<p>
</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Server-driven paywalls on Android: rendering UI your binary has never seen]]></title>
      <link>https://www.revenuecat.com/blog/engineering/server-driven-android-sdk</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/server-driven-android-sdk</guid>
      <pubDate>Thu, 27 Aug 2026 05:26:22 GMT</pubDate>
      <dc:creator><![CDATA[Jaewoong Eum]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[In this article, you'll dive deep into how the RevenueCat Android SDK renders server driven paywalls as native Compose UI, comparing to the WebView screen and what it defers.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/5f81b80ad5012145f45511b3ca454b79d20abb2d-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>The paywall is usually the highest leverage screen in a subscription app, and it is also the screen most tightly welded to your release cycle. Reordering the packages, rewriting the headline above them, or changing which one is preselected means a ticket, a build, a store review, and then waiting for users to update. Server-driven UI breaks that coupling by moving the screen's definition to a server and letting the app render whatever arrives. The moment you do that, though, you inherit a problem the compiler used to solve for you: the server can send your app a screen it has never heard of.</p>
<p>In this article, you'll dive deep into how the <a href="https://github.com/RevenueCat/purchases-android">RevenueCat Android SDK</a> renders server driven paywalls as native Compose UI, comparing to the WebView screen and what it defers, the two level offerings cache and asset pre-download, the three stage pipeline from wire format to render model, how unknown input and misconfiguration get absorbed by different mechanisms, the facts only the device can resolve, and the cost of holding every platform to the same JSON.</p>
<h2><strong>The fundamental problem: The server can send what your binary does not know</strong></h2>
<p>There are broadly two ways to render a screen defined somewhere else. You can ship a runtime that resolves an open-ended instruction set at display time, or you can ship a renderer that understands a closed set of types fixed at compile time. A third option exists, shipping executable code from the server and running it against native widgets, but it trades the schema problem for a second runtime inside your APK and a longer conversation with store policy, so set it aside.</p>
<p>The first option has a ready-made implementation on every Android device. HTML is the instruction set, the browser engine resolves it, and <code>WebView</code> is the host. A paywall becomes a URL. When the design team invents a new layout, they write new markup, the server serves it, and an app that shipped two years ago renders it without ever having been taught what a carousel is.</p>
<p>The second option is what you get when you want native views. You define a schema, you write a renderer for each shape in it, and both live inside your binary. Consider the smallest possible version of that schema:</p>
<pre><code>sealed interface PaywallComponent {
    data class Text(val text: String) : PaywallComponent
    data class Image(val url: String) : PaywallComponent
    data class Stack(val children: List&lt;PaywallComponent&gt;) : PaywallComponent
}</code></pre>
<p>Three shapes, a recursive tree, and a renderer that walks it. This works, and it keeps working right up until the dashboard learns to emit a fourth shape. Now the server sends this to a binary compiled against the three shape schema:</p>
<pre><code class="language-javascript">{
  &quot;type&quot;: &quot;carousel&quot;,
  &quot;pages&quot;: [ { &quot;type&quot;: &quot;text&quot;, &quot;text&quot;: &quot;Unlimited access&quot; } ]
}</code></pre>
<p>Your deserializer has three options if it has to decide alone, and all of them are bad. It can throw, which fails the whole paywall and shows the user nothing on the screen your revenue depends on. It can skip the unknown node, which leaves a hole in the middle of a layout that was designed around it, so the user sees a headline, empty space, and a purchase button with no explanation of what they are buying. Or it can guess, which is worse than either.</p>
<p>Notice what makes this asymmetric. The problem is not rendering. Drawing a carousel in Compose is a solved problem. The problem is that a native renderer knows a closed set of types while the server can emit an open one, and the gap between the two widens every time you ship a dashboard feature, because users update their apps on their own schedule and some of them never will.</p>
<p>This is the actual decision behind native versus WebView paywalls, and it is not settled by a benchmark or a visual audit. Both of those follow from a prior question: where do you put the uncertainty about what the server might send? Inside a runtime you did not write and do not version, or inside a schema boundary you design deliberately?</p>
<h2><strong>The shortcut and what it defers: Hosting a page versus owning a tree</strong></h2>
<p>Before looking at how RevenueCat's SDK answers that question, it is worth accounting for what the WebView approach actually costs, because it genuinely does solve forward compatibility.</p>
<p>A WebView hosted paywall makes the SDK's job small. Its responsibilities reduce to roughly this shape:</p>
<pre><code>webView.loadUrl(paywallUrl)
webView.addJavascriptInterface(bridge, &quot;native&quot;)</code></pre>
<p>The page owns layout, typography, animation, and state. The bridge carries a handful of messages in both directions: the page tells native that the user tapped a product, native tells the page that the purchase succeeded. Everything visual is HTML and CSS, so the design team iterates without touching Kotlin and without a schema negotiation.</p>
<p>What that architecture defers, rather than solves, is everything that depends on the paywall being present, fast, and native.</p>
<p>The paywall becomes a network resource. Before a single pixel can paint, the page has to be fetched. You can pre-warm a WebView to hide this, and well built implementations do, but pre-warming a WebView means keeping a renderer process alive on the speculative chance that the user will see a paywall. You are pre-warming a process. Pre-warming bytes on disk is a different proposition, and the next section is about what that difference buys.</p>
<p>Caching the page yourself is possible, and more possible than it first looks. <code>shouldInterceptRequest</code> lets you serve every request from storage you control, and a service worker gives you explicit eviction and validation. What you cannot do is skip the work. You now maintain a second asset pipeline, its correctness, and its invalidation logic, in a language and a runtime separate from the app hosting it.</p>
<p>The layout engine is not yours to pin. Android System WebView updates independently of your app, through the Play Store, on a schedule set by the user's device, and devices without Play Services, with updates disabled, or on older Chromium builds form a version matrix nobody controls. A CSS rule that lays out correctly during QA can lay out differently on a device that updated its WebView provider last week. Compose does not remove platform variation, since text shaping still comes from <code>StaticLayout</code> and HarfBuzz, but the measure and layout code ships inside your APK, so the box model does not move under you. That is a much narrower moving target than an entire layout engine.</p>
<p>The page's content also sits outside Compose's reasoning. Compose can skip the <code>AndroidView</code> wrapper when its parameters are stable, and the hosted view does report a measured size, but Compose cannot reason about anything inside the document. The page's content height is only known after the document lays out, which happens after Compose has already measured. So a WebView inside a Compose column either gets a height you hardcode or a height that arrives over the bridge one frame late, and nested scrolling between the page's scroll container and a Compose parent becomes a coordination problem rather than a feature.</p>
<p>Finally, the seam between the page and the purchase is untyped. A tap crosses a <code>postMessage</code> boundary as a string, and nothing checks that identifier against your real product catalog until the user presses the button. As you'll see later, the native path checks the same mapping, just before the paywall renders rather than on the tap.</p>
<p>None of this makes the WebView approach a mistake. It makes it a set of deferred payments. The rest of this article walks through what paying them up front looks like in code.</p>
<h2><strong>Nothing to fetch when the paywall opens: Two-level caching and asset predownload</strong></h2>
<p>Start with the property that is easiest to verify and hardest to retrofit. After the first successful offerings fetch, presenting the current offering's paywall no longer depends on the network. The component tree, the fonts, and the images are already on the device.</p>
<p>Offerings, which carry the paywall definitions, are cached at two levels. If you examine <code>OfferingsCache</code>:</p>
<pre><code>internal class OfferingsCache(
    private val deviceCache: DeviceCache,
    private val dateProvider: DateProvider = DefaultDateProvider(),
    private val offeringsCachedObject: InMemoryCachedObject&lt;Offerings&gt; = InMemoryCachedObject(
        dateProvider = dateProvider,
    ),
    private val localeProvider: LocaleProvider,
)</code></pre>
<p>There is an in-memory layer, <code>offeringsCachedObject</code>, for the warm path within a process, and a disk layer behind <code>deviceCache</code> that survives process death. Writing goes to both:</p>
<pre><code>@Synchronized
fun cacheOfferings(offerings: Offerings, offeringsResponse: JSONObject) {
    offeringsCachedObject.cacheInstance(offerings)
    deviceCache.cacheOfferingsResponse(offeringsResponse)
    offeringsCachedObject.updateCacheTimestamp(dateProvider.now)
    cachedLanguageTags = String(localeProvider.currentLocalesLanguageTags.toCharArray())
}</code></pre>
<p>The raw response JSON is persisted, not just the parsed object graph, which means the disk copy can go back through the same parsing path as a fresh network body rather than needing its own deserialization format.</p>
<p>Look at the last line. The cache records which language tags were active when it was populated, and staleness checks both time and locale:</p>
<pre><code>@Synchronized
fun isOfferingsCacheStale(appInBackground: Boolean): Boolean =
    offeringsCachedObject.lastUpdatedAt.isCacheStale(appInBackground, dateProvider) ||
        cachedLanguageTags != localeProvider.currentLocalesLanguageTags</code></pre>
<p>A user who changes their system language gets a refetch even if the cache is chronologically fresh. This is the kind of invalidation rule you only write when the cached payload is locale dependent, and it is the first hint of a theme that runs through the whole architecture: the device is a participant in rendering, not just a display surface.</p>
<p>The disk copy is a failure path rather than a startup path. On a cold start the in memory cache is empty, so the SDK does attempt a network fetch, and what matters is what happens when that fetch fails:</p>
<pre><code>GetOfferingsErrorHandlingBehavior.SHOULD_FALLBACK_TO_CACHED_OFFERINGS -&gt; {
    val cachedOfferingsResponse = offeringsCache.cachedOfferingsResponse
    if (cachedOfferingsResponse == null) {
        handleErrorFetchingOfferings(backendError, onError)
    } else {
        warnLog { OfferingStrings.ERROR_FETCHING_OFFERINGS_USING_DISK_CACHE }
        createAndCacheOfferings(
            offeringsJSON = cachedOfferingsResponse,
            loadedFromDiskCache = true,
            ...
        )
    }
}</code></pre>
<p>Because the disk entry is a complete offerings response, it flows through exactly the same <code>createAndCacheOfferings</code> path as a network body, with <code>loadedFromDiskCache</code> as the only difference the rest of the pipeline sees. A user who opens the app on a plane gets last week's paywall instead of an error screen, so after the first successful fetch no connectivity failure leaves them with no paywall.</p>
<p>Caching the tree is only half of it. A component tree full of remote font and image URLs still paints late if those URLs are fetched when the paywall opens. So the SDK fetches them when offerings are fetched, which is typically during SDK configuration at app launch. Looking at where <code>OfferingsManager</code> handles a successful offerings response:</p>
<pre><code>onSuccess = { offeringsResultData -&gt;
    offeringsResultData.offerings.current?.let {
        offeringImagePreDownloader.preDownloadOfferingImages(it)
    }
    offeringFontPreDownloader.preDownloadOfferingFontsIfNeeded(offeringsResultData.offerings)
    offeringsCache.cacheOfferings(offeringsResultData.offerings, offeringsJSON)
    ...
}</code></pre>
<p>Images and fonts start downloading before the offerings are handed back to the caller. Two limits are visible in those four lines and worth stating rather than glossing: images are predownloaded for <code>offerings.current</code> only, so a paywall on a non-current offering does not get them, and fonts are read from the first offering that has paywall components on the assumption that all offerings share a font set. Both downloads are also asynchronous, so a user who reaches a paywall within the first second of a fresh install can still outrun them.</p>
<p>Fonts are where the distance between native and web is greatest. On the web, a custom font is a single <code>@font-face</code> declaration and the browser handles fetching, caching, validation, and fallback. Android does have downloadable fonts through <code>FontsContractCompat</code>, but that resolves a font name against a provider's catalog. An arbitrary file that a customer uploaded to their own bucket has no platform support at all, and <code>FontLoader</code> is 235 lines of the work the browser was doing for free.</p>
<p>Those 235 lines buy something the section title has been promising: the font file is on disk before the paywall opens, so the first frame draws in the right typeface. Font resolution happens once, before composition, which means no font swap and no flash of invisible text. It also means the guarantee is one directional. A font that is not in the index when the paywall state is built resolves to the system font for the entire life of that presentation, so a missed predownload is not a late swap, it is a paywall rendered in the wrong typeface until the user closes it and opens it again.</p>
<p>The predownloader starts by filtering out fonts it does not need to fetch at all:</p>
<pre><code>private fun isBundled(info: FontInfo.Name): Boolean {
    if (info.value.isEmpty()) return false
    return when (info.value) {
        in genericFonts -&gt; true
        else -&gt; context.getResourceIdentifier(info.value, &quot;font&quot;) != 0 ||
            context.getAssetFontPath(info.value) != null
    }
}</code></pre>
<p>The dashboard can reference a font by name, and the device checks whether that name resolves to a font already bundled in the app's resources or assets before downloading anything. A server could only know this if every app reported its bundled font names on every build.</p>
<p>For fonts that do need downloading, the loader keys its cache by a hash of the URL, so a file referenced by more than one font alias is fetched once and later requests attach themselves to the in flight download as listeners. The download itself verifies integrity and commits atomically:</p>
<pre><code>val tempFile = File.createTempFile(&quot;rc_paywall_font_download_&quot;, &quot;.$extension&quot;, cacheDir)
urlConnectionFactory.downloadToFile(url, tempFile, description = &quot;paywall font&quot;)

val actualMd5 = md5Hex(tempFile.readBytes())
if (!actualMd5.equals(expectedMd5, ignoreCase = true)) {
    tempFile.delete()
    return Result.failure(IOException(&quot;Downloaded font file is corrupt for $url&quot;))
}

if (!tempFile.renameTo(cachedFile)) {
    tempFile.copyTo(cachedFile, overwrite = true)
    tempFile.delete()
}</code></pre>
<p>The file downloads to a temporary name, gets checked against an expected MD5 the server supplied alongside the URL, and only then moves into place. A truncated download never becomes a cache entry, so a partially written font file cannot poison the cache and produce garbled text on every subsequent launch.</p>
<p>There is one more recovery path that only shows up in production. Android can reclaim <code>cacheDir</code> at any time, so a cache entry can point at a file that no longer exists:</p>
<pre><code>if (cachedFontFamily != null) {
    if (cachedFontFamily.fonts.all { it.file.exists() }) {
        return cachedFontFamily
    }
    warnLog { &quot;Cached font files missing for ${cachedFontFamily.family}, re-downloading&quot; }
    ...
}</code></pre>
<p>The in-memory index is checked against the filesystem before being trusted, and a family whose files have been evicted is dropped and refetched rather than handed to Compose as a set of dangling <code>File</code> references.</p>
<h2><strong>The three-stage pipeline: Tolerant, then validated, then rendered</strong></h2>
<p>With assets local, the remaining question is how the JSON becomes Compose UI. The SDK does this in three distinct stages, and the separation is what makes everything after it possible.</p>
<p>The first stage is deserialization into a data model that mirrors the wire format closely and validates almost nothing. <code>PaywallComponentsData</code> is the root:</p>
<pre><code>public class PaywallComponentsData(
    @SerialName(&quot;template_name&quot;) public val templateName: String,
    @SerialName(&quot;asset_base_url&quot;) public val assetBaseURL: URL,
    @SerialName(&quot;components_config&quot;) public val componentsConfig: ComponentsConfig,
    @SerialName(&quot;components_localizations&quot;)
    public val componentsLocalizations: Map&lt;LocaleId, Map&lt;LocalizationKey, LocalizationData&gt;&gt;,
    @SerialName(&quot;default_locale&quot;) public val defaultLocaleIdentifier: LocaleId,
    public val revision: Int = 0,
    @SerialName(&quot;zero_decimal_place_countries&quot;)
    public val zeroDecimalPlaceCountries: List&lt;String&gt; = emptyList(),
    ...
)
</code></pre>
<p>Two things in that declaration shape everything downstream. Localizations arrive as a map keyed by every locale the paywall supports, not as a single pre-resolved language, which makes locale selection the device's job. And <code>revision</code> is carried explicitly, so a client can tell one published version of a paywall from another, which is what makes experiment attribution possible later.</p>
<p>Inside <code>componentsConfig</code> is the tree, built from <code>PaywallComponent</code>, a sealed interface with a hand written serializer:</p>
<pre><code>@Serializable(with = PaywallComponentSerializer::class)
public sealed interface PaywallComponent</code></pre>
<p>The second stage compiles that data model into a render model. <code>StyleFactory</code> performs the transformation, and its constructor shows what compiling means here:</p>
<pre><code>internal class StyleFactory(
    private val localizations: NonEmptyMap&lt;LocaleId, LocalizationDictionary&gt;,
    private val colorAliases: Map&lt;ColorAlias, ColorScheme&gt;,
    private val fontAliases: Map&lt;FontAlias, FontSpec&gt;,
    private val variableLocalizations: NonEmptyMap&lt;LocaleId, NonEmptyMap&lt;VariableLocalizationKey, String&gt;&gt;,
    private val offering: Offering,
    private val stripRules: Boolean = false,
)</code></pre>
<p>Every reference in the wire format gets resolved here. A component whose background is <code>ColorAlias(&quot;primary&quot;)</code> gets an actual <code>ColorScheme</code>. A text component whose font is <code>FontAlias(&quot;brand&quot;)</code> gets a <code>FontSpec</code>. A package component naming a package identifier gets a real <code>Package</code> from the <code>Offering</code>, or an error if no such package exists. String keys become resolved strings for every supported locale. The <code>stripRules</code> flag is the second stage's one concession to unknown input, and the next section is about what sets it.</p>
<p>The output type is deliberately minimal:</p>
<pre><code>@Immutable
internal sealed interface ComponentStyle {
    val visible: Boolean
    val size: Size
}
</code></pre>
<p>By the time a <code>ComponentStyle</code> exists there is nothing left to look up. No alias to resolve, no locale to pick, no package to find. That is what makes it safe to declare <code>@Immutable</code>, and it is why nothing inside composition can fail. The annotation is a promise to the Compose compiler that an instance's properties never change after construction, which lets Compose skip re-executing a composable whose inputs are equal to last time and leave what it emitted in place. Strong skipping means Compose would already skip on instance equality for many of these, but the annotation documents the guarantee and lets the compiler treat the type as stable everywhere it appears rather than only where the same instance happens to be reused.</p>
<p>Validation in this stage accumulates rather than fails fast, and the reason is practical. When a dashboard user publishes a paywall referencing three colors that were deleted, a fail fast validator reports one, the user fixes it, republishes, and discovers the second. Accumulating means one report lists all three. The SDK uses its own <code>Result</code> type:</p>
<pre><code>internal sealed class Result&lt;out A, out B&gt; {
    class Success&lt;A&gt;(val value: A) : Result&lt;A, Nothing&gt;()
    class Error&lt;B&gt;(val value: B) : Result&lt;Nothing, B&gt;()
}
</code></pre>
<p>paired with combinators that gather every error instead of stopping at the first:</p>
<pre><code>internal inline fun &lt;A, B, G, H&gt; zipOrAccumulate(
    first: Result&lt;A, NonEmptyList&lt;H&gt;&gt;,
    second: Result&lt;B, NonEmptyList&lt;H&gt;&gt;,
    transform: (A, B) -&gt; G,
): Result&lt;G, NonEmptyList&lt;H&gt;&gt;</code></pre>
<p><code>zipOrAccumulate</code> takes two results and a function. If both succeeded it applies the function. If either failed it returns every error from both, concatenated. The error type is <code>NonEmptyList&lt;PaywallValidationError&gt;</code>, and the choice of <code>NonEmptyList</code> is what keeps the failure case from lying: a list type that guarantees at least one element makes &quot;failed with zero errors&quot; unrepresentable. <code>PaywallValidationError</code> has twenty one cases, including <code>MissingColorAlias</code>, <code>MissingFontAlias</code>, <code>MissingPackage</code>, <code>MissingStringLocalization</code>, and <code>TabControlNotInTab</code>.</p>
<p>The third stage is rendering, and it is almost boring by comparison. <code>ComponentView</code> dispatches a <code>ComponentStyle</code> to a composable:</p>
<pre><code>@Composable
internal fun ComponentView(
    style: ComponentStyle,
    state: PaywallState.Loaded.Components,
    onClick: suspend (PaywallAction) -&gt; Unit,
    modifier: Modifier = Modifier,
    componentInteractionTracker: PaywallComponentInteractionTracker = PaywallComponentInteractionTracker { _ -&gt; },
) = when (style) {
    is StackComponentStyle -&gt; StackComponentView(...)
    is TextComponentStyle -&gt; TextComponentView(...)
    is ImageComponentStyle -&gt; ImageComponentView(...)
    ...
}
</code></pre>
<p>The remaining branches follow the same shape, and the function ends there. There is no <code>else</code>, and there cannot be a missing case either: a <code>when</code> used as an expression over a sealed type will not compile unless every subtype is covered. Adding a style without a corresponding view is a compile error rather than a runtime hole.</p>
<p>That is the payoff of the three-stage split, and the precise version of the claim is narrower than the grand one. Every question of what type of thing the server sent is answered before stage two ends. What is not settled at that boundary is data. A variable name, an image URL, a window width, and a selected package are all resolved during composition, which is why the misconfiguration path below exists: the types are pinned down at the boundary, the values are not.</p>
<h2><strong>Absorbing bad input: Forward compatibility and misconfiguration are different problems</strong></h2>
<p>That boundary has to actually hold, which brings us back to the carousel arriving at a binary that predates carousels. The SDK has two forward compatibility mechanisms for genuinely unknown input, and then a third, separate path for input that is well formed but misconfigured. Keeping those two problems apart matters, because they fail for different reasons and deserve different answers.</p>
<h3><strong>Tier one: A fallback subtree supplied by the server</strong></h3>
<p>The component deserializer is written by hand rather than generated, and the last branch is where forward compatibility lives:</p>
<pre><code>override fun deserialize(decoder: Decoder): PaywallComponent {
    val jsonDecoder = decoder as? JsonDecoder
        ?: throw SerializationException(&quot;Can only deserialize PaywallComponent from JSON, got: ${decoder::class}&quot;)
    val json = jsonDecoder.decodeJsonElement().jsonObject
    return when (val type = json[&quot;type&quot;]?.jsonPrimitive?.content) {
        &quot;button&quot; -&gt; jsonDecoder.json.decodeFromJsonElement&lt;ButtonComponent&gt;(json)
        &quot;image&quot; -&gt; jsonDecoder.json.decodeFromJsonElement&lt;ImageComponent&gt;(json)
        ...
</code></pre>
<p>Fifteen branches later comes the part that matters:</p>
<pre><code>&quot;fallback_header&quot; -&gt; FallbackHeaderComponent
        else -&gt; json[&quot;fallback&quot;]
            ?.let { it as? JsonObject }
            ?.let { jsonDecoder.json.decodeFromJsonElement&lt;PaywallComponent&gt;(it) }
            ?: throw SerializationException(&quot;No fallback provided for unknown type: $type&quot;)
    }
}</code></pre>
<p>When the type is unrecognized, the deserializer looks for a <code>fallback</code> key on the same object and decodes that value as a <code>PaywallComponent</code>, recursively. So the server does not send a carousel and hope. It sends a carousel with instructions for what to render instead:</p>
<pre><code>{
  &quot;type&quot;: &quot;carousel&quot;,
  &quot;pages&quot;: [ &quot;...&quot; ],
  &quot;fallback&quot;: {
    &quot;type&quot;: &quot;stack&quot;,
    &quot;components&quot;: [ { &quot;type&quot;: &quot;text&quot;, &quot;text_lid&quot;: &quot;carousel_summary&quot; } ]
  }
}
</code></pre>
<p>A binary that knows carousels renders the carousel and ignores <code>fallback</code>. A binary that does not renders the stack. The recursion means a fallback can itself contain a fallback, so a component added in the newest SDK can degrade through intermediate representations down to something a much older binary understands.</p>
<p>The design decision here is that the server owns the degradation choice rather than the client. A client cannot reasonably invent a substitute for a component type it has never seen, but the person who designed the component can specify one, and the dashboard can emit it automatically.</p>
<p>This does not hand an old binary a capability it was never built to render. What it guarantees is that the old binary shows a coherent, purchasable screen instead of a hole, and that a designer chose what that screen is. The hosted approach has a floor of its own here: its instruction set is only as new as the oldest WebView in your install base.</p>
<p>Notice also that every branch decodes from <code>json</code>, the already parsed <code>JsonElement</code>, rather than from text. A comment in the source explains why:</p>
<p>Decode the JsonElement directly; re-stringifying (<code>decodeFromString(json.toString())</code>) is ~quadratic in tree depth, as every nested PaywallComponent would re-stringify its whole subtree.</p>
<p>The obvious implementation of a discriminated union deserializer reads the <code>type</code> field, then hands the JSON text to the right serializer. In a recursive tree, every node on the path from the root down to a leaf re-serializes that leaf, so total work becomes the node count multiplied by the tree's depth instead of just the node count. Ten levels of nesting means the deepest text gets written out and parsed again ten times. Decoding from the parsed element does each node once.</p>
<h3><strong>Tier two: Unknown conditions collapse the override cascade</strong></h3>
<p>The second class of unknown is not a component but a condition. Components carry overrides that apply only in certain situations, which is how one definition covers phone and tablet, light and dark, selected and unselected. Think of a stack of CSS rules applied in source order: each override that matches overwrites the properties it names and leaves everything else alone. <code>ComponentOverride</code> pairs the conditions with the properties:</p>
<pre><code>public class ComponentOverride&lt;T : PartialComponent&gt;(
    public val conditions: List&lt;Condition&gt;,
    public val properties: T,
)
</code></pre>
<p><code>Condition</code> is nested inside <code>ComponentOverride</code> and has grown over time, from simple markers to parameterized rules:</p>
<pre><code>@Serializable(with = ConditionSerializer::class)
public sealed interface Condition {
    public val isRule: Boolean get() = false

    @Serializable public object Compact : Condition
    @Serializable public object Medium : Condition
    @Serializable public object Expanded : Condition
    @Serializable public object IntroOffer : Condition
    @Serializable public object Selected : Condition</code></pre>
<p>The newer ones carry operators and operands, and mark themselves with <code>isRule</code>:</p>
<pre><code>@Serializable
    public data class SelectedPackage(
        public val operator: ArrayOperator,
        public val packages: List&lt;String&gt;,
    ) : Condition { override val isRule: Boolean get() = true }

    @Serializable
    public data class Variable(
        public val operator: EqualityOperator,
        public val variable: String,
        public val value: JsonPrimitive,
    ) : Condition { override val isRule: Boolean get() = true }

    @Serializable public object Unsupported : Condition
}</code></pre>
<p>That <code>isRule</code> flag separates the original fixed set of conditions, which every SDK version has always understood, from the ones added later. The distinction is what the fallback strategy keys on.</p>
<p>Unknown condition types deserialize to <code>Unsupported</code> rather than throwing, through a reusable helper:</p>
<pre><code>internal object ConditionSerializer : SealedDeserializerWithDefault&lt;Condition&gt;(
    serialName = &quot;Condition&quot;,
    serializerByType = mapOf(
        &quot;compact&quot; to { Condition.Compact.serializer() },
        &quot;selected_package_condition&quot; to { Condition.SelectedPackage.serializer() },
        &quot;variable_condition&quot; to { Condition.Variable.serializer() },
        ...
    ),
    defaultValue = { Condition.Unsupported },
)</code></pre>
<p><code>SealedDeserializerWithDefault</code> is used at seven boundaries, six of them in the component schema, and it falls back in two situations rather than one:</p>
<pre><code>val serializer = type?.let { serializerByType[it] }
    ?: return defaultValue(type ?: &quot;null&quot;)
return try {
    jsonDecoder.json.decodeFromJsonElement(serializer(), jsonObject)
} catch (_: Exception) {
    defaultValue(type)
}</code></pre>
<p>An unrecognized discriminator falls back. So does a type the client recognizes whose payload fails to parse, which means a future server version can add a required field to an existing condition without breaking older clients. The unknown value half of this pattern also appears on enums through <code>EnumDeserializerWithDefault</code>, and on nested sealed types like <code>PurchaseButtonComponent.Method</code>, which carries an explicit <code>Unknown</code> case alongside <code>InAppCheckout</code> and the web checkout variants. It is not applied to every sealed type in the schema, though: <code>PaywallComponent</code> itself throws when no fallback is present, and types like <code>Dimension</code> and <code>ColorInfo</code> are plain polymorphic with no default.</p>
<p>Now, what should a client do when it finds an <code>Unsupported</code> condition? Rendering the override anyway is wrong, because the condition it depended on was never evaluated. Ignoring only that override is also wrong, and the reason takes a moment to see. Say one override paints a dark background on compact screens, and a second one, carrying a rule, switches the text to light when the annual package is selected. Drop the second and the text keeps its base dark colour on the dark background the first one painted, which nobody ever previewed. Overrides are written as a stack, and half a stack is not a smaller design, it is a different one.</p>
<p>So the effect escalates from the override to the whole tree. Any <code>Unsupported</code> condition anywhere sets a single flag, and that flag discards every override carrying a rule across the entire paywall:</p>
<pre><code>internal fun &lt;T : PartialComponent, P : PresentedPartial&lt;P&gt;&gt; List&lt;ComponentOverride&lt;T&gt;&gt;.toPresentedOverrides(
    stripRules: Boolean = false,
    transform: (T) -&gt; Result&lt;P, NonEmptyList&lt;PaywallValidationError&gt;&gt;,
): Result&lt;List&lt;PresentedOverride&lt;P&gt;&gt;, PaywallValidationError&gt; {
    val overridesToProcess = if (stripRules) {
        this.filter { override -&gt;
            override.conditions.none { it.isRule || it is ComponentOverride.Condition.Unsupported }
        }
    } else {
        this
    }
    ...</code></pre>
<p>What survives is the base set of conditions, so the paywall renders as the design without conditional refinements. The trade off is explicit: a coarser but coherent paywall instead of a precisely targeted but internally inconsistent one. The cost is a real deployment coupling. The flag is computed per component tree, so on the day the dashboard starts emitting a new condition type, every client that has not yet updated loses conditional refinement across the whole of any paywall that uses it, not just the component that carried the new condition.</p>
<p>Once conditions are resolvable, the cascade itself is a fold:</p>
<pre><code>internal fun &lt;T : PresentedPartial&lt;T&gt;&gt; List&lt;PresentedOverride&lt;T&gt;&gt;.buildPresentedPartial(
    windowSize: ScreenCondition,
    offerEligibility: OfferEligibility,
    state: ComponentViewState,
    conditionContext: ConditionContext = ConditionContext(null, emptyMap()),
): T? {
    var partial: T? = null
    for (override in this) {
        if (override.shouldApply(windowSize, offerEligibility, state, conditionContext)) {
            partial = partial.combineOrReplace(override.properties)
        }
    }
    return partial
}</code></pre>
<p>Overrides apply in declaration order, and <code>combineOrReplace</code> merges each matching one over the accumulated result, replacing outright only when there is nothing accumulated yet. Precedence is an explicit list order rather than a scoring algorithm, which makes it deterministic and testable.</p>
<h3><strong>The separate path: Misconfiguration exits the components pipeline</strong></h3>
<p>The two mechanisms above handle input the client does not recognize. A different failure is input that is perfectly well formed and simply wrong: a color alias that references a deleted color, a package identifier that is not in the offering, a tabs component with no tabs. That is not a forward compatibility problem, it is a misconfiguration problem, and it gets a different answer.</p>
<p><code>validatedPaywall</code> is where all of it converges:</p>
<pre><code>internal fun Offering.validatedPaywall(
    currentColorScheme: ColorScheme,
    resourceProvider: ResourceProvider,
): PaywallValidationResult =
    validatePaywallComponentsDataOrNull(resourceProvider)?.let { result -&gt;
        when (result) {
            is RcResult.Success -&gt; result.value
            is RcResult.Error -&gt; fallbackPaywall(currentColorScheme, resourceProvider, errors = result.value)
        }
    } ?: paywall?.validate(currentColorScheme, resourceProvider)
        ?: fallbackPaywall(currentColorScheme, resourceProvider, error = PaywallValidationError.MissingPaywall)</code></pre>
<p>Four outcomes in one expression. A component tree that validates is used. A component tree that fails validation goes to <code>fallbackPaywall</code>. An offering with no component tree falls through to the older <code>PaywallData</code> paywall it may have configured. An offering with neither also goes to <code>fallbackPaywall</code>.</p>
<p>The result type is what makes the exit clean:</p>
<pre><code>internal sealed interface PaywallValidationResult {
    val errors: NonEmptyList&lt;PaywallValidationError&gt;?

    data class Legacy(
        val displayablePaywall: PaywallData,
        val template: PaywallTemplate,
        override val errors: NonEmptyList&lt;PaywallValidationError&gt;? = null,
    ) : PaywallValidationResult


and on the components branch, a comment stating the policy directly:

data class Components(...) : PaywallValidationResult {
        // If a Components Paywall has an error, it will be reflected as a Legacy type so we can use the Legacy
        // fallback.
        override val errors: NonEmptyList&lt;PaywallValidationError&gt;? = null
    }</code></pre>
<p><code>Components</code> structurally cannot carry errors. A components paywall either compiled cleanly or it is no longer a components paywall, and there is no third state for the renderer to interpret.</p>
<p>The name <code>fallbackPaywall</code> suggests something more configured than it is. It builds <code>PaywallData.createDefault(availablePackages, ...)</code> against <code>PaywallData.defaultTemplate</code>, and because that result carries errors, what actually draws is <code>DefaultPaywallView</code>, which reads only the package list off it. The generated screen is nicer than it sounds: it pulls the app's name and icon from the platform and derives two prominent colors from the icon, so it lands approximately on brand with no configuration at all. It lists real products at real prices with a working purchase button, and the diagnostic explaining what went wrong is a debug build only overlay, so a production user sees a plain paywall rather than an error message aimed at a developer.</p>
<p>The shape across all three mechanisms comes down to this. Tiers one and two are forward compatibility, absorbing input from a newer server than the binary expects, and both stay inside the components renderer. The misconfiguration path is robustness against a badly configured dashboard, and it leaves the components renderer entirely. What they share is that none of them reaches <code>ComponentView</code> as an unknown type, which is why stage three needs no <code>else</code> and no defensive branch.</p>
<h2><strong>What the node tree gives you: Semantics and a typed purchase path</strong></h2>
<p>Everything so far has been about surviving the schema boundary. Two things that become possible on the other side of it are hard to get any other way.</p>
<p>The first is accessibility. Because components render as Compose nodes, they emit semantics that TalkBack consumes directly, and the SDK can shape that tree deliberately. <code>StackComponentView</code> splits a clickable stack into three parts. A wrapper <code>Box</code> carries the caller's modifier and owns both the click gesture and the semantics. Inside it, the stack draws its content without a shape clip so that children which intentionally overflow, like offset badges, stay visible, and a sibling supplies the shape clipped ripple. Putting the click and the semantics together on the wrapper merges them into a single node alongside whatever <code>testTag</code> or <code>Role</code> the caller supplied, rather than scattering them into separate nodes a screen reader has to announce one at a time.</p>
<p>A WebView is not inaccessible, and pretending otherwise would be easy to refute. ARIA gives explicit control over roles, labels, and grouping. What it gives you is a second accessibility contract to get right, one that does not compose with the native tree around it: focus order across the <code>AndroidView</code> boundary, <code>mergeDescendants</code> on a Compose parent, and tag based UI tests all stop at the edge of the document. The native tree also means the paywall is testable with <code>SemanticsNodeInteraction</code>, the same tooling as the rest of the app, instead of needing a browser automation layer.</p>
<p>The second is the purchase path. The purchase button is not a generic button posting a string across a bridge, it is a typed component with a typed action:</p>
<pre><code>@SerialName(&quot;purchase_button&quot;)
public class PurchaseButtonComponent(
    public val stack: StackComponent,
    public val action: Action? = null,
    public val method: Method? = null,
    public val name: String? = null,
) : PaywallComponent {
    public enum class Action {
        IN_APP_CHECKOUT,
        WEB_CHECKOUT,
        WEB_PRODUCT_SELECTION,
    }</code></pre>
<p>By the time this reaches the renderer it has been compiled into a style holding a resolved <code>Package</code>, which came from the <code>Offering</code>, which came from Google Play. A dashboard that references a package not in the offering produces <code>MissingPackage</code> in stage two and exits to the fallback path before a user can tap anything. This is not compile time safety on the product identifier, since the identifier is data. It is the same check a bridge would eventually do, moved from the button press to before the paywall renders.</p>
<h2><strong>What only the device can know: Where the division of labor falls</strong></h2>
<p>There is a common misreading of server driven UI, which is that the server draws the screen and the client displays it. That is not what is happening here, and the difference is why the on device work is not incidental.</p>
<p>The dashboard owns design intent. The device owns facts nobody can know at publish time. The schema is the contract between them, and conditions are how intent gets expressed in terms of facts the server does not have.</p>
<p>Start with theme, because it shows the pattern in four lines. Colors arrive from the server as a pair rather than a value:</p>
<pre><code>public class ColorScheme(
    public val light: ColorInfo,
    public val dark: ColorInfo? = null,
)</code></pre>
<p>and resolution happens inside composition:</p>
<pre><code class="language-kotlin">internal val ColorStyles.forCurrentTheme: ColorStyle
    @Composable
    get() = if (isSystemInDarkTheme()) dark ?: light else light</code></pre>
<p>Reading <code>isSystemInDarkTheme()</code> inside a composable registers a recomposition dependency on the configuration, so a theme change updates the paywall in place, provided the host activity declares <code>android:configChanges=&quot;uiMode&quot;</code> and is not recreated outright. Locale works the same way and is held as Compose state:</p>
<pre><code>private var localeId by mutableStateOf(initialLocaleList.toLocaleId())

val locale by derivedStateOf { localeId.toComposeLocale() }</code></pre>
<p>Every text component reading <code>locale</code> recomposes with strings from a different entry in the localizations map that was already downloaded, because the wire format shipped all of them.</p>
<p>Screen size comes from the standard adaptive API rather than a guess about device class:</p>
<pre><code>internal enum class ScreenCondition {
    COMPACT, MEDIUM, EXPANDED;

    companion object {
        fun from(sizeClass: WindowWidthSizeClass) =
            when (sizeClass) {
                WindowWidthSizeClass.COMPACT -&gt; COMPACT
                WindowWidthSizeClass.MEDIUM -&gt; MEDIUM
                WindowWidthSizeClass.EXPANDED -&gt; EXPANDED
                else -&gt; {
                    Logger.d(&quot;Unexpected WindowWidthSizeClass: '$sizeClass'. Falling back to COMPACT.&quot;)
                    COMPACT
                }
            }
    }
}</code></pre>
<p>That <code>else</code> is instructive after a section about exhaustive <code>when</code> expressions. <code>WindowWidthSizeClass</code> is a plain class with a private constructor and companion object constants rather than a sealed type or an enum, so there is no exhaustiveness to lean on and the branch is mandatory. Note where it falls back to, and note that it logs. Because the value comes from the current window rather than the physical display, a foldable that unfolds or an app entering split screen moves between conditions live.</p>
<p>The case that no server can resolve at all is offer eligibility. A dashboard author wants a headline reading &quot;Start your free trial&quot; for users who qualify and &quot;Subscribe&quot; for users who already used theirs. Eligibility is a property of this user's purchase history on this store, so the device resolves it by reading the shape of what Google Play returned:</p>
<pre><code>internal val Package.introOfferEligibility: OfferEligibility
    get() {
        val phaseCount = (product.defaultOption?.pricingPhases?.size ?: 0) - 1

        return when (phaseCount) {
            1 -&gt; OfferEligibility.IntroOfferSingle
            2 -&gt; OfferEligibility.IntroOfferMultiple
            else -&gt; OfferEligibility.Ineligible
        }
    }</code></pre>
<p>The number of pricing phases beyond the base phase tells you how many discounted phases this user can actually receive, because Google Play only includes phases they are eligible for. The <code>else</code> treats an offer with three or more discounted phases as ineligible rather than inventing a category for it, which is a real limit in an otherwise total function. Promotional offers layer on top with the same logic and fall back to intro eligibility when no promo applies, and the result becomes the <code>offerEligibility</code> argument to <code>buildPresentedPartial</code>, which is what makes an <code>IntroOffer</code> condition evaluable at all.</p>
<p>Currency formatting is the subtlest of these, and a good illustration of why the device has to decide. A user's language and their storefront country are independent, so a Korean speaker can have a US storefront. Format a derived per month price in the wrong locale and the separator or symbol placement disagrees with the price string the store returned, which users notice immediately. The lookup tries three things in order: a locale that already pairs the device's language with the storefront's country, then any locale for that storefront, then one built by stapling the storefront's region onto the device's language.</p>
<pre><code>val currencyLocale by derivedStateOf {
    if (storefrontCountryCode.isNullOrBlank()) {
        locale
    } else {
        val deviceLanguageCode = locale.language.lowercase()

        val javaLocale = availableStorefrontCountryLocalesByLanguage[deviceLanguageCode]
            ?: availableStorefrontCountryLocalesByLanguage.values.firstOrNull()
            ?: Locale.Builder()
                .setLocale(locale.toJavaLocale())
                .setRegion(storefrontCountryCode.uppercase())
                .build()

        javaLocale.toComposeLocale()
    }
}
</code></pre>
<p><code>availableStorefrontCountryLocalesByLanguage</code> is built once by scanning <code>Locale.getAvailableLocales()</code> for every locale whose country matches the storefront. Text renders in the device language, prices format according to the storefront country, and <code>zeroDecimalPlaceCountries</code> from the wire format suppresses decimals in currencies that do not use them.</p>
<p>Localization covers more than text. The payload is a three case hierarchy:</p>
<pre><code>public sealed interface LocalizationData {
    public value class Text(public val value: String) : LocalizationData
    public value class Image(public val value: ThemeImageUrls) : LocalizationData
    public value class Video(public val value: ThemeVideoUrls) : LocalizationData
}
</code></pre>
<p>A localization key can resolve to a string or to a set of image URLs, which means a Korean user and a US user can see entirely different hero imagery from one paywall definition, chosen on the device with the assets already predownloaded. The <code>Video</code> case is declared and read by the renderer, but the deserializer only attempts <code>Text</code> then <code>Image</code>, so it is not currently reachable from the wire. That is what a schema running slightly ahead of its parser looks like from the inside, and it is the same coordination problem the platform section returns to.</p>
<p>Finally, <code>UiConfig</code> adds a token layer above individual paywalls:</p>
<pre><code>public class UiConfig(
    public val app: AppConfig = AppConfig(),
    public val localizations: Map&lt;LocaleId, Map&lt;VariableLocalizationKey, String&gt;&gt; = emptyMap(),
    @SerialName(&quot;variable_config&quot;) public val variableConfig: VariableConfig = VariableConfig(),
    @SerialName(&quot;custom_variables&quot;) public val customVariables: Map&lt;String, CustomVariableDefinition&gt; = emptyMap(),
) {
    public class AppConfig(
        public val colors: Map&lt;ColorAlias, ColorScheme&gt; = emptyMap(),
        public val fonts: Map&lt;FontAlias, FontsConfig&gt; = emptyMap(),
    )</code></pre>
<p>Components reference <code>ColorAlias</code> and <code>FontAlias</code> rather than literal values, so changing a brand color once updates every paywall referencing it without touching a component. <code>FontsConfig</code>, whose declaration is not in that excerpt, holds a single field named <code>android</code>. The name is the interesting part: a schema serving only Android would not need to key the field by platform at all. The wire format carries a font entry per platform and each SDK reads its own key, which is the first place in the schema where cross-platform parity shows up as a design constraint rather than an implementation detail.</p>
<p>The <code>localizations</code> map on <code>UiConfig</code> is separate from a paywall's own copy and holds translations for variable rendering. When a component writes <code>{{ product.period }}</code>, the word &quot;month&quot; has to appear in the reader's language even though the dashboard author only wrote English. Those translations ship with the config and get applied on the device.</p>
<p>So the division of labor is specific. The dashboard decides what the paywall means. The device decides what is true right now. Three of those facts a hosted page can also read for itself: <code>navigator.language</code>, <code>prefers-color-scheme</code>, and a media query cover locale, theme, and width without any bridge. Theme comes with a wrinkle, since what a WebView reports to <code>prefers-color-scheme</code> follows the host theme's <code>android:isLightTheme</code> rather than the system setting directly. The other three are the ones a browser cannot see on its own. What this user's purchase history makes them eligible for, which currency conventions this storefront uses, and which fonts already shipped inside this binary all have to arrive as bridge messages the page then re-renders against, and until they arrive the page is displaying a guess.</p>
<h2><strong>The platform behind the JSON: What the SDK does not show you</strong></h2>
<p>Reading through the SDK, it is easy to conclude the hard part is the renderer. It is not. The renderer is the visible half of a system whose expensive half never ships to a device.</p>
<p>Start with the editor. A dashboard where a product manager rearranges a paywall has to emit a component tree valid against the schema, with live preview, which means a browser side implementation of the same layout semantics the native renderers implement. Visual editors for general purpose layout are a solved product category. An editor for a schema you invented is not one of them. It is a third renderer, and it has to stay in parity with the other two.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/d5c4ed35bef45f668543490b3530360ee9cdd150-1722x1265.png" alt=""/></figure>
<p>Then the schema. <code>PaywallComponentsData</code> carries a <code>revision</code>, and a good number of its sealed types have default cases, because the schema is a contract between one server and every SDK version ever installed. <code>SealedDeserializerWithDefault</code> covers seven of them, and others like <code>ButtonComponent.Action</code> and <code>ButtonComponent.Destination</code> reach the same result through hand written serializers that map an unrecognized value to <code>Unknown</code>.</p>
<p>Adding a component type is not a code change, it is a protocol change. The type has to be designed, added to the schema, implemented in each platform renderer, given a sensible fallback representation for older clients, taught to the editor, and covered by fixtures. The <code>fallback</code> mechanism from tier one only works because someone designs the fallback at the same time as the component.</p>
<p>Then parity, where the cost is highest and least visible, because it is not a feature you finish. The same JSON has to produce the same paywall on Android, on iOS, and through every hybrid framework in between, across codebases with different layout systems. Compose and SwiftUI do not agree by default about how a stack distributes remaining space, how a shadow interacts with a clipped shape, or what a percentage size resolves to inside a scroll container. Getting them to agree is deliberate work, and keeping them agreeing is permanent work.</p>
<p>Those resources feed the debug source set, where <code>TemplatePreviews</code> renders them through a preview parameter provider so a change to the renderer can be reviewed against the same definitions every platform consumes. Separately, <code>ui/revenuecatui-testing</code> publishes <code>PaywallFixtures</code>, <code>PaywallFixtureView</code>, <code>PaywallFixtureViewOptions</code>, and <code>PaywallFixturesTestRule</code>, which let an app developer snapshot test their own dashboard configured paywalls against recorded fixtures. Parity is not maintained by discipline alone; there is a shared corpus and there is tooling that renders it.</p>
<p>On top of rendering, an experimentation platform needs to know what happened. The SDK carries <code>PaywallEvent</code>, <code>PaywallStoredEvent</code>, <code>PaywallPresentedCache</code>, <code>PaywallPostReceiptData</code>, and <code>PaywallComponentInteractionTracker</code>, which threads through most component views. Attribution is the reason: to know whether variant B beat variant A, a purchase has to be tied back to the exact paywall revision on screen when it happened, surviving process death, backgrounding, and a store callback that arrives later.</p>
<p>Add it up and the shape is clear. The JSON is the easy part. A visual editor, a versioned schema with a designed degradation story, a delivery and caching tier, a renderer per platform held to parity by a shared corpus, and an experiment assignment and attribution pipeline are the actual system. Parity is also not a cost you pay once: every component type you add adds work on every platform you support, indefinitely.</p>
<p>This is the honest argument for not building it yourself. Not that the rendering is hard, though the degradation design took real thought, but that the rendering is the part you would actually finish. RevenueCat provides the dashboard, the schema, the delivery, the renderers, the experiment engine, and the attribution, which is why a product manager can reorder packages, change which one is preselected, swap a hero image for one locale, or restyle a paywall and have it take effect on installed apps without a release. That capability is not a feature of the SDK. It is a property of the whole platform, and the SDK is the part that happens to run on the device.</p>
<h2><strong>The bill, honestly: Release gates, schema ceilings, and parity</strong></h2>
<p>Native server-driven UI is the more expensive architecture, and there are four places the cost lands.</p>
<p>The first is that new capabilities are gated on releases. When a component type is added, apps cannot render it until they ship an SDK version that knows it, and the <code>fallback</code> mechanism exists precisely because that transition takes months across a real install base. A hosted paywall has no such gate. That is a genuine advantage, and the fallback subtree does not erase it, it only guarantees the old binary shows something coherent in the meantime. The gap is not a fee that purchases anything either. It is the same architectural decision seen from the other side: a render model living inside your binary is what makes the offline path, the theme and locale response, and the pre-render validation possible, and it is what makes new components wait for a release.</p>
<p>The second is a ceiling on expression, and it is the one a competitor leads with. A designer can only produce effects the schema models. If the schema has no notion of a particular animation curve or a blend mode, no amount of dashboard work produces it, and the request becomes a protocol change with a multiplatform release behind it. A hosted page has no such ceiling, because its instruction set is the whole of CSS.</p>
<p>The third is the deployment coupling described in tier two. One unknown condition type strips every override that carries a rule across the whole of the affected paywall, which means the day a new condition ships, clients that have not updated lose conditional refinement throughout that paywall rather than only where the new condition appeared.</p>
<h2><strong>Conclusion</strong></h2>
<p>The next time you evaluate a server driven UI system, the question to ask is not how it renders. It is where the system puts the uncertainty about what the server might send, and who pays when that uncertainty resolves badly. A hosted document pushes it into a runtime you do not version, and the bill arrives as a blank screen on a bad network, a layout that shifted when the WebView provider updated, or a product identifier that did not match anything until a user pressed the button. A typed component tree pushes it into a schema boundary you design, and the bill arrives as release coordination, a ceiling on expression, and renderer parity, paid by the team rather than the user.</p>
<p>What makes the second approach hold up is that the boundary is a single, narrow place, and that every way past it has a named floor. Past that boundary, <code>ComponentView</code> is a <code>when</code> with no <code>else</code>, and the compiler will not let the renderer be incomplete. Every architecture has a point where it stops guessing. It is worth choosing where yours is.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Matt Birchler’s Apps folder has 99 folders. Most were never meant to ship.]]></title>
      <link>https://www.revenuecat.com/blog/growth/matt-birchler-indie-app-dev-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/matt-birchler-indie-app-dev-launched-podcast-2026</guid>
      <pubDate>Wed, 26 Aug 2026 13:26:01 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Matt Birchler used a Sketch template, SwiftUI, and rough early AI tools to become a native app developer — and today, his software earns more than either his blog or his YouTube channel.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/efc44e22dfbe573485b1bce93d6ae1d937f2e68f-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>For years, Matt Birchler was known as a prolific tech blogger, YouTuber, and podcaster—not as a native iOS developer. He had a degree in television production, an early career in retail management and tech support, and one rough Swift weather app sitting at the bottom of his App Store Connect account as a permanent reminder of his first attempt.</p>
<p>Then he got tired of opening Sketch just to write a movie review.</p>
<p><a href="https://www.youtube.com/watch?v=ofradaWNzf8">Watch on YouTube</a></p>
<p>That inconvenience eventually became Quick Reviews, the first app Matt considers a serious release. It also started a run of small utilities built from the same question: if a piece of software keeps annoying you, why not fix it yourself?</p>
<h2><strong>A Sketch artboard had to become an app</strong></h2>
<p>Before Quick Reviews was an iOS app, it was a manual design chore. Matt wanted to post visually appealing reviews of movies and games to his social accounts, so he built a Sketch document. Every time he wanted to post a review, he had to sit at his Mac, duplicate the artboard, swap the poster image, and retype the text.</p>
<p>When people started asking what app he was using, he built a simple, one-page jQuery website to automate the process. It still required users to screenshot the final result, and it had no account system or review history. He wanted the tool on his phone, which meant he finally had to figure out native iOS development.</p>
<h2><strong>Bad AI made him learn the code</strong></h2>
<p>Matt knew CSS and HTML, but his previous attempt at building an iOS app felt like &quot;laying out your HTML in tables.&quot; SwiftUI's declarative syntax felt familiar to someone who already knew the web. At the same time, early AI coding tools like Cursor were becoming available.</p>
<p>He spent about a month and a half working through the app, but the AI tools available at the time were frustratingly rudimentary.</p>
<p>“It was so hard. You had to tell it to do one small thing. It would screw it up. You'd have to do it again... The entire app would be in one Swift file, and you'd have to remind it, ‘you might want to do a second file.’” — Matt Birchler</p>
<p>But that friction was exactly what he needed. Because the AI couldn't build the app perfectly on the first try, Matt was forced to read the output, understand the data structures, and learn how the views interacted. That structural knowledge paid off. Today, when he uses much better AI tools, he actually knows what to ask for and how to verify that the generated code is correct.</p>
<h2><strong>The $10 subscription was downside protection</strong></h2>
<p>Quick Reviews is a free app, but it includes a $10 annual subscription from day one. That decision wasn't driven by aggressive growth targets; it was driven by API exposure.</p>
<p>The app pulls metadata, posters, and cast information from The Movie Database and uses an RSS feed to import a user's recent Letterboxd reviews. Relying on third-party services meant Matt couldn't assume every dependency would remain free forever.</p>
<p>“I wanted to make sure all of these projects do not lose me money.” — Matt Birchler</p>
<p>By putting the automated metadata and Letterboxd sync behind a paywall immediately, he gave the app a way to cover future service costs. Separately, the wider app catalog has become Matt's largest side-income stream, bringing in more revenue than either his blog or his YouTube channel by a significant margin.</p>
<h2><strong>99 folders of experiments</strong></h2>
<p>Once Matt realized he could fix his own software annoyances, the development bug bit hard. He built Quick Subtitles and Chapterize to remove friction from his own podcast workflow, using them multiple times a week. When he set a goal to run 365 miles in a year and couldn't find a workout app that simply graphed his progress toward that specific target, his immediate response was, &quot;I could make that.&quot; So he built Yearly Run Goals.</p>
<p>He names his apps clearly—using words like &quot;Quick&quot;—to signal that they are focused utilities, not massive platforms that require endless support. But he doesn't release everything he builds.</p>
<p>In his Mac's home directory, Matt has a folder simply named &quot;Apps.&quot; It contains 99 folders—not all of them projects he built himself, but many of them tiny tools, abandoned visualizers, and experiments that never went anywhere. He sees a friction point and wants to know whether he can fix it. Sometimes the answer is yes, and a new app ships. Sometimes the answer is no, and the experiment stays in the folder.</p>
<h2><strong>Where to go next</strong></h2>
<p>In <a href="https://www.youtube.com/watch?v=ofradaWNzf8" target="_blank" rel="noopener noreferrer">the full episode</a>, Matt also discusses why actually selling software on the App Store changed his view of Apple's 30% commission, why YouTube sponsorships often create more administrative work than they are worth, and how co-hosting the Comfort Zone podcast gives him a collaborative creative outlet.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://birchtree.me/">Birchtree</a></li>
<li><a href="https://apps.apple.com/us/app/quick-reviews/id6740369719">Quick Reviews on the App Store</a></li>
<li><a href="https://www.youtube.com/@ABetterComputer">A Better Computer on YouTube</a></li>
<li><a href="https://www.macstories.net/comfort-zone/">Comfort Zone Podcast</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to create ultra-personalized paywalls with Apple's Foundation Models]]></title>
      <link>https://www.revenuecat.com/blog/engineering/how-to-create-ultra-personalized-paywalls-with-apple-s-foundation-models</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/how-to-create-ultra-personalized-paywalls-with-apple-s-foundation-models</guid>
      <pubDate>Wed, 26 Aug 2026 09:43:46 GMT</pubDate>
      <dc:creator><![CDATA[Rhys Kentish]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Use Apple's on-device Foundation Models and RevenueCat custom variables to build a paywall that speaks to each user: free, private, and shipped in an afternoon.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/ea41aac83a6d3d94d66ade6f1b00c768200a9358-1720x914.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>You know personalized paywalls convert better, but what if you could use everything your app already knows about the user to get really (but not inappropriately so) personal?</p>
<p>Timing makes this worth the effort: according to our <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps 2026 report</a>, 55% of 3-day trial cancellations happen on Day 0. The battle for a subscriber is won or lost in the first session, so if your paywall only gets one shot, it should speak to the person looking at it.</p>
<aside class="tip"><strong>Make sure you're using the correct versions</strong><p>Before getting started, make sure your app runs a compatible version of the RevenueCat SDK. Custom variables on the iOS SDK require version <a href="https://github.com/RevenueCat/purchases-ios/releases/tag/5.57.0">5.57.0</a>. You'll also need to target iOS 26 or above.</p></aside>
<h2>The example app</h2>
<p>Chorus, the bird song identifier and walk tracker I'm building for <a href="https://shipaton.com/">Shipaton 2026</a>, spends its onboarding learning why someone is using the app and what would get them to walk more. It asks about their dream bird, who they go on walks with, when they go on walks, and what would actually get them out the door more.</p>
<p>This post is about what happens next: <strong>using Apple's on-device Foundation Models framework to hand their own ’why’ back to them at the moment of the ask</strong>, on a RevenueCat remote paywall. The best part? It's free.</p>
<p>Here's the basic paywall with no personalization:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/247ea5837ed5de40d09a13675895e45c9ddf7c6c-945x2048.jpg" alt="Chorus' basic paywall with no personalisation"/><figcaption>Chorus' basic paywall with no personalisation</figcaption></figure>
<p>It does the job. It's okay, but it's not <em>really</em> speaking to the user. Let's change that.</p>
<h2>Custom variables on the paywall</h2>
<p>Using RevenueCat remote paywalls, you can start personalizing them using <a href="https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls/variables#custom-variables">custom variables</a>, but what if you could push that further? What if you could use everything your app already knows about your customer to <em>really</em> get personal?</p>
<p>Open your RevenueCat paywall editor. Click ‘Paywall Logic’ in the left-hand menu and select Variables. Then click ‘Create Variable’ and you'll see the following entry form:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/935e76f275d2a702e774d384ee2ca1047b8a28f6-798x792.jpg" alt="New custom variable form screenshot on the paywall editor"/></figure>
<p>I'll start by creating the userName variable (possibly the easiest bit of personalization), with the default value ‘Birder’. Chorus asks for the birder's name in onboarding, saves it, and passes it into the remote paywall in Swift like so:</p>
<pre><code class="language-swift">struct ChorusPlusPaywall: View {
    let userName: String?   // asked for in onboarding; nil if they skipped it

    var body: some View {
        PaywallView(displayCloseButton: true)
            // If there's no name, send nothing; the dashboard
            // default (&quot;Birder&quot;) fills the gap.
            .customPaywallVariables(userName.map { [&quot;userName&quot;: .string($0)] } ?? [:])
    }
}
</code></pre>
<p>That'll produce a paywall like this:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/14322d27cd4e64cd2c0e5ae682dd931cf484058c-945x2048.jpg" alt="Chorus' paywall using custom variables"/><figcaption>Chorus' paywall using custom variables</figcaption></figure>
<p>Better. But we can make it even better with Foundation Models.</p>
<h2>Apple's Foundation Models</h2>
<p>Foundation Models is Apple's on-device large language model (LLM) framework, introduced in iOS 26. It generates text locally, with no server round trip and no per-token cost. Apple says &quot;On-device models excel at a diverse range of text generation tasks, like summarization, entity extraction, text and image understanding, refinement, dialog for games, generating creative content, and more&quot;. Perfect for our use case. On-device models are much smaller than cloud-based models, so there are limitations, which I'll go through later.</p>
<p>As I mentioned, Chorus gets to know the customer during onboarding — this is where the magic happens.</p>
<p>Create a class that handles all interaction with Foundation Models. I've called mine ‘PaywallCopyGenerator’. In Chorus, each onboarding screen is aware of this class, and its functions get called at different points in the flow.</p>
<p>Right at the start, the welcome screen calls prewarm(), which checks SystemLanguageModel.default.availability and warms a session. Devices without Apple Intelligence bail out here: they ship the hand-written fallback line and never touch the model.</p>
<pre><code class="language-swift">func prewarm() {
  guard warmSession == nil else { return }
  guard case .available = SystemLanguageModel.default.availability else { return }

  warmSession = LanguageModelSession(instructions: instructions)

  warmSession?.prewarm()
}</code></pre>
<p>Prewarming the session reduces the time it takes for the user to see generated output. Do it as early as possible.</p>
<p>One gotcha: the first time a user's device meets the requirements, SystemLanguageModel can register as unavailable while the OS downloads the model in the background. Don't panic, it resolves itself. In Chorus I don't need to guard with an iOS 26 availability check because the app targets iOS 26; if you support older versions, you will.</p>
<h2>Instructions and prompts</h2>
<p>You'll notice you pass system instructions into the session. Apple has great guidance on getting the best out of the model by <a href="https://developer.apple.com/documentation/foundationmodels/prompting-an-on-device-foundation-model#Give-the-model-a-role-persona-and-tone">giving the model a role, persona, and tone</a>. Here are Chorus's system instructions:</p>
<pre><code class="language-swift">You are a warm British field naturalist writing paywall copy for Chorus, a
bird-walking app. You are given notes about a walker from the app's
onboarding: their dream bird, what would get them out walking more, and how
they walk. Write two short sentences spoken to them as &quot;you&quot;.
1. Conjure their next walk as one small imagined moment, built only from
the notes: who is with them, when or where they walk, their dream bird
singing somewhere in it.
2. Say what Chorus can help them do about it.
Never invent people, places, times or history that are not in the notes.
Mention Chorus exactly once, in the help sentence; never name features or
prices. Never the walker's name. No questions, no exclamation marks, no em
dashes. Thirty words at most.

Write like a person talking, not an advert: plain, everyday words, said
simply, nothing you would not say to a friend on a walk. The examples
below are style only; never reuse their scenes, company or times of day
for a different walker. When the notes say who walks with them, hand it
back as theirs: &quot;your partner&quot;, &quot;your dog&quot;, &quot;your kids&quot;, not &quot;a partner&quot;
or &quot;the kids&quot;.

Example 1.
Notes: Their dream bird: the goldfinch; they have never heard one; that is
the dream. What would get them out more: knowing new birds were waiting.
They walk at dawn, with their kids. They get out less than they'd like.
Line: Imagine a dawn walk with your kids when a goldfinch strikes up, your
first ever. Chorus can help you catch it the moment it sings.

Example 2.
Notes: Their dream bird: the wren; they hear them all the time and love
them. What would get them out more: keeping the birds they've found close.
They notice birds by ear first.
Line: Picture the next wren that sings before you spot it, one more song
worth keeping. Chorus can help you hold on to every one.

Example 3.
Notes: Their dream bird: the blackbird; they hear them all the time and
love them. What would get them out more: calmer, quieter time outside.
They walk at dusk, as the day's song winds down.
Line: Imagine a quiet walk at dusk, just you and the blackbird seeing the
day out. Chorus can help you make more of those.</code></pre>
<p>The instructions lean on five concepts from Apple's documentation:</p>
<ol>
<li><strong>The persona:</strong> Chorus follows Apple's role-playing blueprint. Casting the model as a <em>&quot;warm British field naturalist&quot;</em> with the words <em>&quot;you are&quot;</em> merges character and voice in a single line. The instructions are also written in the exact register they expect back, right down to banning em dashes and obeying that ban themselves.</li>
<li><strong>Step-by-step logic:</strong> dense task descriptions are broken into a two-step plan. For smaller models, this reduces the cognitive load and keeps sequencing on track.</li>
<li><strong>Size constraints:</strong> the prompt stays lean. Two paragraphs of imperatives and one clear objective fit Apple's recommended length budget.</li>
<li><strong>Few-shot safety:</strong> three curated examples establish the style. An <em>&quot;examples are style only&quot;</em> guard prevents exemplar bleed. Without it, the model happily hallucinates dawn walks for night owls.</li>
<li><strong>Strategic repetition:</strong> high-stakes rules like <em>&quot;never invent&quot;</em> and <em>&quot;exactly once&quot;</em> are reinforced, and the runtime prompt repeats the most important rule last.</li>
</ol>
<p>The runtime prompt works differently. Onboarding captures information about the user, then a few screens before the paywall we ask the model to generate the personalized line. That head start matters: on-device models are still fairly slow, so generating early means the line is ready by the time the paywall appears. (You can add a loading state at the end of onboarding if you prefer, but it's best not to block the paywall.)</p>
<p>Apple suggests sending the model hard facts, so a fairly long Swift function builds the runtime prompt with no conditional language (no ‘if’s for the model to reason about). Deterministic code decides which facts apply; the model only translates them into prose:</p>
<pre><code class="language-swift">// Chorus — the walker's brief. Apple's &quot;turn conditional prompting into programming
// logic&quot;, shipped: deterministic Swift turns onboarding answers into prose notes, so
// the on-device model only ever sees the conditions that apply. The model's whole job
// is translating these notes into one warm line. From `PaywallCopyGenerator.swift`.

/// The walker's why, in prose: dream bird + history, what would get them out more,
/// how they notice birds, and every context line that exists. Empty when everything
/// was skipped — there is nothing to reflect, and the fallback line ships instead.
func brief(signals: OnboardingSignals) -&gt; String {
    var lines: [String] = []
    if let bird = signals.dreamBird {
        let heard: String = switch signals.dreamBirdHistory {
        case .often: &quot;they hear them all the time and love them&quot;
        case .yearsAgo: &quot;they heard one once, years ago&quot;
        case .unsure: &quot;they aren't sure they've ever heard one&quot;
        default: &quot;they have never heard one; that is the dream&quot;
        }
        let seen: String? = switch signals.dreamBirdSeen {
        case .often: &quot;they see them all the time&quot;
        case .yearsAgo: &quot;they saw one once, years ago&quot;
        case .unsure: &quot;they couldn't say if they've seen one&quot;
        case .never: &quot;they have never seen one&quot;
        case nil: nil
        }
        // Short name — briefing &quot;the Common Nightingale&quot; teaches stilted formal names.
        lines.append(&quot;Their dream bird: the \(bird.shortName); \(heard)&quot;
                     + (seen.map { &quot;, and \($0)&quot; } ?? &quot;&quot;) + &quot;.&quot;)
    }
    switch signals.desiredPerk {
    case .whereNext: lines.append(&quot;What would get them out more: knowing new birds were waiting.&quot;)
    case .cameraScan: lines.append(&quot;What would get them out more: being able to name what they see.&quot;)
    case .wearBird: lines.append(&quot;What would get them out more: keeping the birds they've found close.&quot;)
    case .knowNow: lines.append(&quot;What would get them out more: knowing what's singing around them, right there and then.&quot;)
    case .everyDay: lines.append(&quot;What would get them out more: a reason to get out every day.&quot;)
    case .calm: lines.append(&quot;What would get them out more: calmer, quieter time outside.&quot;)
    case .learnSongs: lines.append(&quot;What would get them out more: learning to know the songs themselves.&quot;)
    case nil: break
    }
    switch signals.spotting {
    case .ear: lines.append(&quot;They notice birds by ear first.&quot;)
    case .eye: lines.append(&quot;They see birds but often can't name them.&quot;)
    default: break
    }
    switch signals.knowTheBirds {
    case .everyOne: lines.append(&quot;They want to know every bird they've heard.&quot;)
    case .special: lines.append(&quot;They'd like to know the special ones at least.&quot;)
    case .justListening: lines.append(&quot;They're happy just listening.&quot;)
    case nil: break
    }
    if let habitat = signals.habitat {
        switch habitat {
        case .gardenAndStreet: lines.append(&quot;They walk gardens and streets close to home.&quot;)
        case .park: lines.append(&quot;Their walks loop the local park.&quot;)
        case .woodland: lines.append(&quot;Their walks run under trees more often than not.&quot;)
        case .water: lines.append(&quot;Their walks keep close to water.&quot;)
        case .openCountry: lines.append(&quot;They walk open country where song carries far.&quot;)
        case .allOver: break
        }
    }
    switch signals.walkTime {
    case .dawn: lines.append(&quot;They walk at dawn, when the chorus is loudest.&quot;)
    case .dusk: lines.append(&quot;They walk at dusk, as the day's song winds down.&quot;)
    default: break
    }
    switch signals.companion {
    case .alone: lines.append(&quot;They walk alone.&quot;)
    case .partner: lines.append(&quot;They walk with their partner.&quot;)
    case .dog: lines.append(&quot;They walk with their dog.&quot;)
    case .kids: lines.append(&quot;They walk with their kids.&quot;)
    default: break
    }
    switch signals.walkFrequency {
    case .mostDays: lines.append(&quot;They walk most days.&quot;)
    case .fewTimesAWeek: lines.append(&quot;They walk a few times a week.&quot;)
    case .nowAndThen: lines.append(&quot;They get out now and then.&quot;)
    case .lessThanLike: lines.append(&quot;They get out less than they'd like.&quot;)
    case nil: break
    }
    return lines.joined(separator: &quot; &quot;)
}</code></pre>
<p><code>OnboardingSignals</code> is a struct that gets populated as the customer moves through onboarding.</p>
<p>The final piece of the puzzle is asking the model to generate output:</p>
<pre><code class="language-swift">func generate(signals: OnboardingSignals) {
    guard task == nil else { return }

    let brief = self.brief(signals: signals)
    guard !brief.isEmpty else { return }   // everything skipped → fallback line

    working = true
    task = Task { [weak self] in
        defer { self?.working = false }
        guard let self else { return }
        self.generatedLine = await self.requestLine(
            prompt: &quot;Notes: \(brief)\nUse only these notes. Write the line.&quot;)
    }
}

private func requestLine(prompt: String) async -&gt; String? {
    guard let session = warmSession else { return nil }   // no model → fallback
    let response = try? await session.respond(
        to: prompt,
        generating: GeneratedLine.self,
        options: GenerationOptions(temperature: 0.85, maximumResponseTokens: 110))
    return response?.content.line
}
</code></pre>
<p>The runtime prompt is simple compared to the system instructions. We ask the model to fill a <code>@Generable</code> type:</p>
<pre><code class="language-swift">@Generable
private struct GeneratedLine {
    @Guide(description: &quot;Both sentences: the reflection, then the Chorus help line. Nothing else&quot;)
    var line: String
}
</code></pre>
<p>You can do interesting things here too, like checking the output for banned words and regenerating when one appears. You may also want to trim whitespace. The model occasionally leaks JSON artifacts into the string, so I trim it like this:</p>
<pre><code class="language-swift">let line = raw.trimmingCharacters(in: .whitespacesAndNewlines)
    .trimmingCharacters(in: CharacterSet(charactersIn: &quot;{}\&quot;\u{201C}\u{201D}&quot;))
    .trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>
<p>Pass the generated line into the paywall like before and you should see the foundation models generate something like the text surrounded by the red rectangle:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/701c87262fa947edefb7e728ca5d88eade415c42-704x1526.jpg" alt="Chorus' paywall with the peronalized paywall body"/></figure>
<p>Pretty cool, right?</p>
<h2>Limitations of Apple’s Foundation Model for personalized paywall copy</h2>
<p>Foundation models are small. I like to compare them to the early days of GPT-3: about three billion parameters against the trillions in server-side models. Output isn't always great, and they can hallucinate. That's why the safety checks matter. Out of the box, Apple protects against harmful output (racism, violence, profanity), but Apple also states that you're responsible for the content the model generates.</p>
<p><strong>A note on prompt safety:</strong> Chorus never sends freeform user text to the model. The brief is built entirely from fixed onboarding answers, so there's nothing a user can type that ends up in the prompt. If you do include free text, treat it as untrusted: interpolate it into a rigid prompt structure, and never let it stand alone as instructions.</p>
<p>I also found the model isn't yet smart enough for more ambitious moves, like picking the most relevant feature for a customer and spotlighting it. But it's early days: the models improved massively from iOS 26 to iOS 27, and I expect that trend to continue.</p>
<p>One more reason to ground the model in the user's own answers: our SOSA 2026 data shows <a href="https://www.revenuecat.com/blog/growth/ai-feature-cost-subscription-app-margins">AI-powered apps earn 41% more per payer but churn 30% faster</a>. AI features only pay off when they create real, lasting value. Personalization built from what users actually told you is exactly that.</p>
<h2>Going even further with AI-personalized paywalls</h2>
<p>From iOS 27 you can swap the on-device model for a cloud-based one through the <a href="https://developer.apple.com/documentation/foundationmodels">Foundation Models framework</a>. <a href="https://github.com/anthropics/ClaudeForFoundationModels">Anthropic's Claude already conforms</a>, and the other big providers may follow suit.</p>
<p>There's also Apple's Private Cloud Compute, which runs much larger server-side models. You can use it for free if you have fewer than two million lifetime first-time downloads and are enrolled in the <a href="https://www.revenuecat.com/docs/platform-resources/apple-platform-resources/app-store-small-business-program">App Store Small Business Program</a>.</p>
<p>With those larger models, the more ambitious personalization opens up, like picking the most relevant feature for each customer and spotlighting it. Teams like Tinder invest heavily in exactly this kind of paywall relevance work; with these APIs, you don't need a dedicated team to try it.</p>
<p>Whether you want ultra-personalized paywall copy that calls out a birder’s favorite feathered friend, or are looking to demo the exact feature that solves your user’s job-to-be-done, on-device AI is unlocking a new way to tailor paywalls to every individual — just in time to ask them to subscribe.</p>
<p>
To keep going on paywalls, <a href="https://www.revenuecat.com/blog/growth/paywalls-study-guide/">start with our paywalls study guide</a> or <a href="https://www.revenuecat.com/docs/tools/paywalls">check out the paywalls documentation</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How (and why) I monetized a game with a rewarded ad and no paywall]]></title>
      <link>https://www.revenuecat.com/blog/engineering/monetizing-without-a-paywall-using-ads</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/monetizing-without-a-paywall-using-ads</guid>
      <pubDate>Thu, 20 Aug 2026 19:46:34 GMT</pubDate>
      <dc:creator><![CDATA[Austin Blake]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Thirty seconds of video, thirty minutes of access.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/db778cd43187f5d4c630d90ba8a1952347759c89-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>My wife knows very well that I’m someone who is determined to find the best parking spot in a lot. We started talking, as I circled parking lots, about some of my criteria for a great spot, and I thought: “This would make a great game.” So I built it. It's called <em>Curb Appeal</em>. You get 10 rounds, a parking lot, a timer, and one job: pick the best spot.</p>
<p>But the interesting part isn't the game. It's how it makes money. There's no paywall, no subscription, no in-app purchase (IAP). There's one button that says 'Watch Ad to Play', and behind that button is RevenueCat Ads.</p>
<p>Keep in mind that although I used this to make a game, RevenueCat Ads can also work for an app you’re building, such as a Photo Editing app that lets you watch an ad to unlock a premium filter, or a Messaging app that unlocks premium sticker packs with ads.</p>
<p><a href="https://www.youtube.com/watch?v=6bIhAyKVpPw">Watch on YouTube</a></p>
<h2>RevenueCat Ads does two things right now</h2>
<h3>Ad revenue tracking ties ad money to the same user as your purchases</h3>
<p>The obvious pitch is that you stop bouncing between AdMob and RevenueCat to figure out what your app actually made. True, and nice. But the better pitch is underneath that: <strong>your ad revenue gets tied to the </strong><em><strong>same user</strong></em><strong> as your subscription and IAP data</strong>.</p>
<p>That means ad revenue folds into your revenue chart and into realized LTV. You get <a href="https://www.revenuecat.com/blog/growth/measure-hybrid-monetization/">blended revenue per user</a> instead of two disconnected numbers you're eyeballing across two tabs. There's also a dedicated ads section in <a href="https://www.revenuecat.com/feature/charts/">RevenueCat Charts</a> and an ads tab on individual customer profiles, so &quot;is this free user actually worth anything?&quot; becomes a question you can just look up.</p>
<p>Francie wrote the full breakdown of the tracking side when it went into public beta: <a href="https://www.revenuecat.com/blog/growth/track-in-app-ad-revenue-alongside-purchases-get-the-full-picture-on-monetization">every metric, what shows up where, and how the SDK integration works</a>. If you're on AdMob, it's mostly swapping your ad loading calls for RevenueCat's loadAndTrack methods. Make sure to turn on ‘Impression-Level Ad Revenue’ for it to track correctly.</p>
<p>Start there if tracking is what you came for. The rest of this post is the other half.</p>
<h3>Ad rewards grant virtual currency or a timed Entitlement (this is new)</h3>
<p>A user finishes a rewarded ad, AdMob fires a server-side verification (SSV) callback to RevenueCat, and RevenueCat verifies it and grants the reward. You don't build the verification layer. You don't build the granting layer. You just configure it in the dashboard.</p>
<p>Two kinds of rewards:</p>
<ul>
<li><strong>Virtual currency:</strong> coins, credits, gems, tokens, whatever your economy uses</li>
<li><strong>Entitlements:</strong> actual RevenueCat Entitlements, granted for a duration</li>
</ul>
<p>That second one is the interesting part. An Entitlement is just a level of access. Normally you get one by paying, but now, you can earn one by watching an ad. Same Entitlement check in your code either way.</p>
<h2>Before you start: what you need in place</h2>
<p>Three prerequisites:</p>
<ol>
<li>An AdMob account with a rewarded ad unit, connected to your RevenueCat project so your ad units sync to the dashboard.</li>
<li>Server-side verification enabled on that ad unit in the AdMob console, pointing at RevenueCat's SSV callback URL. It's one shared endpoint for every ad unit, and RevenueCat works out which unit and which user from what the SDK attaches at show time.</li>
<li>The RevenueCat SDK (purchases-ios 5.80.3 or later (iOS 15+), or purchases-android 10.12.0 or later).</li>
</ol>
<p>The AdMob adapter that makes this a two-line change is iOS and Android only. On Flutter, React Native, Unity, or Kotlin Multiplatform, you take the manual path: generate a verification token, attach it to your ad's SSV options, then poll for the result. That needs purchases_flutter 10.6.0+, react-native-purchases 10.5.0+, purchases-unity 9.8.0+, or purchases-kmp 3.5.0+.</p>
<p>Ad revenue also doesn't count toward Monthly Tracked Revenue right now, which means the feature is free to use.</p>
<p>The <a href="https://www.revenuecat.com/docs/ad-monetization">ad monetization docs</a> walk through every step. Point your agent at them: it can read the docs, do the implementation, and debug it when something's off (that’s what I did!).</p>
<h2>How to set up RevenueCat Ads Entitlements in your dashboard</h2>
<p><strong>1. Create the Entitlement:</strong> Product catalog → Entitlements → new Entitlement. I called mine <code>play_game</code>, because that's what it does.</p>
<p>One thing worth knowing: you do not need to attach a product to that Entitlement for ad rewards to work. But you also don't have to choose between ads and purchases for the same Entitlement — you can attach both. A user gets access either by subscribing or by watching an ad, and your code only ever checks one thing: is play_game active.</p>
<p>Say you're running a Pro tier that gates your whole app behind a subscription. Attach an ad reward to that same pro Entitlement, and now a non-payer can earn Pro access for 30 minutes by watching an ad, on the exact same Entitlement your subscribers already have.</p>
<p>For an ads-only Entitlement with no purchase path at all, you can skip the product entirely.</p>
<p><strong>2. Create the reward:</strong> Ads → Rewards → 'Add entitlement reward'. Pick your synced rewarded ad unit, pick the Entitlement, set a duration.</p>
<p>I went with 30 minutes, which is the lowest amount of time allowed. Watch one ad, play for the next half hour.</p>
<p><strong>3. Wire up verification, then check the Entitlement:</strong> On the ad itself you call <code>enableRewardVerification()</code> after it loads, then present it with a <code>rewardVerificationCompleted</code> callback. That callback is where you react to the verified reward. The <a href="https://www.revenuecat.com/docs/ad-monetization/rewards">rewards docs</a> have the full Swift and Kotlin versions.</p>
<p>Gating the game is the same check you'd write for a subscription:</p>
<p><code>let customerInfo = try await Purchases.shared.customerInfo()</code></p>
<p><code>let canPlay = customerInfo.entitlements[&quot;play_game&quot;]?.isActive == true</code></p>
<p>You don't have to refresh anything yourself. Before it hands you the result, the SDK already refreshes customer info for an Entitlement reward and invalidates the virtual currencies cache for a currency reward. We’ll just want to make sure we check this value each time before we allow the user into the game.</p>
<p>That's it! Watch ad → Entitlement active → button goes live → 30 minutes of aggressive parking decisions.</p>
<h2>Or, grant virtual currency instead of a timed Entitlement</h2>
<p>Duration-based isn't always the right shape. If 30 minutes of access doesn't map to how your app works, use virtual currency instead: 10 coins per ad, 10 coins per game. Users can stack up a balance, spend it how they want, and you're not fighting a timer to model something that isn't time-based.</p>
<p>An ad unit can have one virtual currency reward and multiple Entitlement rewards at the same time, and completing the ad grants all of them together. So you can hand out coins <em>and</em> open up access from a single view if that's your design.</p>
<h2>When rewarded ads are the wrong call</h2>
<p>A rewarded ad only helps you if it's not replacing a sale you'd have made anyway. If someone would have subscribed or paid specifically for the perk behind the ad, don't let them earn that same perk for free by watching thirty seconds of video. However, a subscriber who watches an ad for something extra — bonus currency, a one-time boost, a perk they wouldn't otherwise pay for, is pure upside. More engagement, more ad revenue, nothing cannibalized.</p>
<p>RevenueCat's <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps 2026</a> found only about 10% of apps run a true <a href="https://www.revenuecat.com/blog/growth/ai-hybrid-monetization/">hybrid monetization model</a> that mixes IAP, ads, and subscriptions, and it's concentrated in gaming at roughly 4x the average. That's where adoption is today, not where it has to stay. The rewarded ad pattern maps cleanly into non-gaming apps too, and most haven't tried yet. This gap is an opportunity.</p>
<p>Execution is what decides whether it pays off. The reward has to be valuable: earn something people actually want. It has to be discoverable: if it's buried two menus deep nobody finds it. And it has to be repeatable: a reward users come back for compounds, which is where you find the incremental dollars.</p>
<h2>Quick tangent… enter Shipaton to win the Catvertising Award!</h2>
<p>There's a <a href="https://www.revenuecat.com/blog/company/announcing-shipaton-2026">Shipaton</a> category for exactly this. It's called the <a href="https://www.shipaton.com/categories/catvertising-award">Catvertising Award</a>, and it goes to the <strong>most creative and effective use of RevenueCat Ads</strong> as a monetization method. Judges are looking for clever placements, smart integration with the rest of your revenue stack, and an experience users don't hate.</p>
<p>First place is $20,000, second is $10,000, third is $5,000. The winner also gets a Shippy trophy, an invitation to RevenueCat's App Growth Annual conference in New York City, coverage on 9to5Mac and 9to5Google, and their app on a giant billboard in Times Square.</p>
<p>Two rules worth knowing before you start: the app has to be released for the first time between August 1 and September 30, 2026, and it has to use the RevenueCat SDK for a purchase or for ads. Submissions close September 30 at 11.45pm PDT.</p>
<p>If you've got a game, or any app where watching an ad to keep going makes sense, this is a low-effort category to enter and there's a lot on the table.</p>
<p><a href="https://www.shipaton.com/categories/catvertising-award">See the Catvertising Award category →</a></p>
<h2>Go build something</h2>
<p>The thing I keep coming back to: ads and subscriptions were never really an either/or. Now both paths run through the same Entitlement, show up in the same revenue chart, and roll into the same LTV number.</p>
<p>Set it up, then go make something great!</p>
<h2><strong>Further Reading</strong></h2>
<ul>
<li><a href="https://www.revenuecat.com/blog/growth/track-in-app-ad-revenue-alongside-purchases-get-the-full-picture-on-monetization">Track in-app ad revenue alongside purchases</a></li>
<li><a href="https://www.revenuecat.com/blog/growth/measure-hybrid-monetization">Blended ARPU Framework: How to Measure Hybrid Monetization</a></li>
<li><a href="https://www.revenuecat.com/blog/growth/hybrid-monetization-techniques">How subscription apps can use hybrid monetization to capture more revenue</a></li>
</ul>
<h3><strong>Learn more in the Docs</strong></h3>
<ul>
<li><a href="https://www.revenuecat.com/docs/ad-monetization">Ad monetization overview</a></li>
<li><a href="https://www.revenuecat.com/docs/ad-monetization/rewards">Granting ad rewards</a>: SSV setup, dashboard config, Swift and Kotlin samples</li>
<li><a href="https://www.revenuecat.com/docs/ad-monetization/admob">AdMob ad tracking</a></li>
<li><a href="https://www.revenuecat.com/docs/ad-monetization/manual-integration">Manual ad tracking</a>: AppLovin MAX, ironSource, Unity Ads, and other ILRD platforms</li>
<li><a href="https://www.revenuecat.com/docs/integrations/third-party-integrations/google-admob">Google AdMob integration</a>: connecting your AdMob account</li>
<li><a href="https://www.revenuecat.com/docs/offerings/virtual-currency">Virtual currency</a></li>
<li><a href="https://www.revenuecat.com/docs/getting-started/entitlements">Entitlements</a></li>
<li><a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/ads">Ad charts</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Visible has never sent a lifecycle email. It's at $10M ARR.]]></title>
      <link>https://www.revenuecat.com/blog/growth/luke-martin-fuller-visible-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/luke-martin-fuller-visible-sub-club-podcast-2026</guid>
      <pubDate>Wed, 19 Aug 2026 13:41:57 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Luke Martin-Fuller left almost every lever in the subscription growth playbook untouched — and Visible is now hiring someone to pull them.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/4629e8910fb519767062e008e3952f8bed51cfe5-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Visible started monetizing in October 2023. Once it crossed $1M in subscription ARR, it took exactly two years to reach $10M. That part is a good number. The list of things the company skipped on the way there is the interesting part.</p>
<p>&quot;We've never sent a lifecycle email. We haven't done any experimentation on our web funnel,&quot; says co-founder Luke Martin-Fuller. &quot;We haven't tested any paywalls. We haven't expanded in our geographies. We've not added AdWords or TikTok or SEO or AEO.&quot;</p>
<p><a href="https://www.youtube.com/watch?v=DnzG2OvRflI">Watch on YouTube</a></p>
<p>One paid channel — Meta, running user-generated creative — carries the entire acquisition motion. Roughly half of new customers still arrive through word of mouth. Visible sells in two markets, the US and the UK. There has been no price testing.</p>
<p>The reasoning is sequencing, not contrarianism. &quot;The thinking really was that unless you've got something of value that is going to be retaining people and helping them, then there's no point investing in all of that experimentation,&quot; Martin-Fuller says.</p>
<p>David Barnard, who has lived with a chronic illness for over 15 years and came to the product as a skeptic, puts the failure mode in blunter terms. &quot;A colleague of mine calls them paywall wrappers. If the product isn't real, you just got 60 pages of onboarding that hype somebody up, they subscribe, and then they get zero value and don't retain.&quot;</p>
<p>Visible built the free app first and ran it for over a year before charging anyone. It reached roughly 50,000 users on word of mouth alone, with no wearable attached. Every one of those untouched levers is still sitting there, which is a strange position for a $10M business to be in — and an unusually good one for whoever takes the growth role Visible is currently hiring for.</p>
<h2><strong>The web quiz is partly there to talk people out of buying</strong></h2>
<p>All of Visible's traffic lands on a landing page, then into a quiz that went live in October 2023 and hasn't changed since. Martin-Fuller is the first to say it's under-optimized. But it does one job deliberately.</p>
<p>&quot;The truth is that our product isn't for everyone,&quot; he says. &quot;And it would be inconsistent with what we're trying to do in the world to just make people buy it.&quot;</p>
<p>The quiz screens for whether pacing — the energy-management strategy the product is built around — is actually the right approach for that person. Some of them get told no.</p>
<p>Barnard's argument for why this isn't self-sabotage is the sharpest passage in the episode: customers who pay but never get value leave worse reviews, kill retention, and feed you misleading product signals about what to build next. &quot;Yes, you may be able to juice conversion, but you're going to kill retention.&quot;</p>
<p>The supporting number: median time from first landing on the site to purchase is 10 days. People go away and research. Some of them, Martin-Fuller notes, now send an agent to do it for them. For a considered purchase, a long deliberation window isn't a leak in the funnel. It's the qualifying mechanism working.</p>
<h2><strong>Members submit an audition tape, then get paid cash for the ads that ship</strong></h2>
<p>Visible runs two creative programs with a team of two people.</p>
<p>The first, Creator Collaborative, works with established creators already in the chronic illness space — compensated on reach or impressions depending on their following. The second, Community Voices, recruits from Visible's own customers. A line in the monthly member digest invites people to join a Slack group. They submit an audition tape. From then on they get a creative brief every week and can send in a video.</p>
<p>The compensation detail is the one worth stealing. &quot;If we use it, then they'll get paid some money,&quot; Martin-Fuller says. &quot;And that's cold, hard cash. That's not credits or a discount on a future purchase.&quot; It's a flat fee, identical whether the video becomes the top performer or gets $10 of spend and never runs again.</p>
<p>Everything gets tested on small budgets first. Only one or two videos a week ever receive scaled spend.</p>
<p>The program also produced a hire. Visible's social media manager, Gemma, first came into contact with the company when they asked her to make a video. She lives with chronic illness herself and was already an influencer in the space. They enjoyed working with her enough to offer her a job.</p>
<p>Martin-Fuller thinks they're leaving something on the table by keeping all of it inside paid. The approach he wants to test is letting members post the same videos organically on TikTok, unapproved and unbriefed, on the theory that one in a hundred goes somewhere.</p>
<h2><strong>The band costs $80 because Visible makes nothing on it</strong></h2>
<p>Visible sells a Polar-manufactured armband at roughly $80, bought wholesale, at cost. The subscription is $20 a month, or $14.99 annually.</p>
<p>&quot;We don't make any money on the hardware that we sell,&quot; Martin-Fuller says. &quot;The way we deliver value is by building an amazing app experience.&quot;</p>
<p>Barnard's reaction on first seeing the price was suspicion — $80 reads as a cheap knockoff until you learn there's no markup on it at all. Which is a real positioning risk, and Martin-Fuller concedes it's worth revisiting.</p>
<p>He lays out three models the team looked at. Charge a lot upfront for the device and keep the recurring fee low — the Oura approach, which works partly because a ring functions as jewelry, and where a premium ceramic variant makes the standard one feel affordable by comparison. Price everything into a single rolling membership and never mention hardware cost, which is Whoop's model, though you're still committing to 12 or 24 months upfront. Or take the third path, closer to what dog-tracker company Tractive does: drop the entry barrier as far as it goes and make the money on the ongoing service.</p>
<p>Selling hardware also removed the free trial entirely. You can't give away a physical device, so trial length, trial opt-in placement, and the rest of that playbook are simply unavailable. Visible's substitute is the original free app — still live, genuinely useful, and deliberately barely promoted. One small mention on the website, fed by word of mouth, and it accounts for a large share of acquisition.</p>
<h2><strong>The research papers are the part nobody clones</strong></h2>
<p>From the earliest version of the free app, Visible let users opt in to sharing anonymized data with researchers. Doing it properly meant informed-consent flows, ethics committee sign-off via an academic partner, and pipelines that strip identifying data while preserving what makes it useful. None of that moved a revenue number.</p>
<p>What it produced was papers in journals including Nature, on using heart-rate data to predict future symptom events, and work with Mount Sinai and Imperial College London on the interaction between symptoms and the menstrual cycle.</p>
<p>For a community that has spent years being disbelieved, and a category crowded with people selling supplements and courses, that record is the trust argument. Visible is careful about what it claims: &quot;It's not a treatment, it's not a cure. It helps you implement a management strategy.&quot;</p>
<p>It's also the thing a competitor can't reproduce quickly. Asked about clones, Martin-Fuller is relaxed. &quot;Honestly hope they don't try because they won't get that far.&quot;</p>
<p>In <a href="https://www.youtube.com/watch?v=DnzG2OvRflI" target="_blank" rel="noopener noreferrer">the full episode</a>, Luke and David also get into why Visible chose not to announce its Series A, the natural-language AI feature that early access users rejected outright, and how a startup ended up giving product feedback to a company that has been building heart rate monitors since 1977.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://www.linkedin.com/in/luke-martin-fuller/">Luke Martin-Fuller on LinkedIn</a></li>
<li><a href="https://apps.apple.com/us/app/visible-pacing-for-illness/id1624474919">Visible</a></li>
<li><a href="https://makevisible.notion.site/">Visible careers</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[We studied 3,500+ AI-powered apps to see why some retain users better than others]]></title>
      <link>https://www.revenuecat.com/blog/growth/ai-app-retention-study</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/ai-app-retention-study</guid>
      <pubDate>Tue, 18 Aug 2026 12:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Margarita Loktionova]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[AI apps monetize better than traditional subscription apps, but they also churn faster. We looked at what the highest-retaining AI apps have in common.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/9c7047700992073f99f1118e1c6fcc60d0f19d8d-2400x1275.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>One in four subscription apps on the App Store and Google Play is now AI-powered, per our <a href="https://revenuecat.com/state-of-subscription-apps/">State of Subscription Apps report</a>. They also monetize better than the rest of the market: 41% more revenue per payer in the first year ($30.16 vs. $21.37 median Y1 LTV). </p>
<p>The honeymoon doesn't last, though. AI apps churn at 30% higher rates than traditional subscription apps. The novelty that converts so well wears off just as fast.</p>
<p>Or is there more to it?</p>
<p>The category average doesn't tell us the whole story. Plenty of AI apps are actually retaining at the level of non-AI subscription businesses.</p>
<p>So we went looking for what separates that group from the rest.</p>
<h2>The gap between the retention groups</h2>
<p>We analyzed 3,519 AI-powered apps using RevenueCat's subscription data and ranked them by how well they retain paying subscribers a year in. We then grouped them into three retention categories: high-, mid-, and low-retention.</p>
<p><strong>→</strong> <a href="#how-we-grouped-and-analyzed-the-ai-apps">Here's how we identified and grouped the apps.</a></p>
<p>High-retention AI apps keep 13.9% of paid subscriptions active after a year, at the median. Mid-retention apps keep 5.3%. Low-retention apps keep 1.4%, a tenth of the top group.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/0ed0b50e46ed659ef08522a46c7313ce45190405-1921x1080.png" alt="Apps were divided into low, mid, and high groups using the bottom 30%, middle 40%, and top 30% of the fair-comparison retention score."/></figure>
<p>The same ranking holds across all plan lengths:</p>
<ul>
<li>On <strong>monthly plans</strong>, the high group retains 10.9%, compared with an AI-app average of 6.1% and a non-AI average of 9.5%.</li>
<li>On<strong> annual plans</strong>, it retains 30.7%, exactly matching the non-AI benchmark and far above the 21.1% AI average.</li>
</ul>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/befff0cc83df5b2d9f90b4f85a0a00f805bab27c-1921x1080.png" alt="One-year retention is higher within every plan duration: weekly climbs from 0.2% to 1.1% to 3.1%, monthly from 1.6% to 4.2% to 10.9%, and annual from 8.2% to 16.5% to 30.7%, across low, mid, and high groups."/></figure>
<p>So, where in the year does that gap build up? Mostly at the first renewal.</p>
<p>On monthly plans, 57.9% of subscribers at high-retention apps renew the first time; at low-retention apps, it's only 30.2%. Later renewals narrow the gap — 79.5% versus 68.5% by the third month.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ed547d888c9bf5d4d567b403a32f3a5394cfbe8d-1921x1080.png" alt="Median renewal rate by renewal opportunity, weekly and monthly plans, by retention group"/></figure>
<h2>What separates high- and low-retention AI apps</h2>
<p>Here's every trait we measured, ranked by how much more common it is in the high-retention group than the low-retention group:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/5be2435b48a87f4351297872cd937088beb01daa-2882x1620.png" alt="Where high- and low-retention apps differ most, ranked. All figures show how much more common a trait is in the high-retention group than in the low-retention group, in percentage points."/></figure>
<ul>
<li><strong>Monetization structure</strong>: Subscription-only apps are 16.4 percentage points more common among high retainers, while hybrid monetization is 16.3 points more common among low retainers. But a quarter of the best-retaining AI apps still sell consumables alongside a subscription.</li>
<li><strong>Trials</strong>: A 7-day trial runs 12.7 points more common among high retainers; no trial at all runs 23.7 points more common among low retainers. But the value of a trial depends on plan length: it favors trials strongly for weekly plans, mildly for monthly, and much less so for annual.</li>
<li><strong>Access model</strong>: Freemium runs 11.4 points more common among high retainers (66.8% versus 55.4%). That said, hard paywalls are rare among all AI apps, at under 2% of apps.</li>
<li><strong>Launch year</strong>: Apps launched between 2020 and 2023 are 10.6 percentage points more prevalent in the high-retention group. By contrast, apps launched in 2024 or later are 20.2 points more prevalent among low retainers. This could partly be due to the recent AI boom and the influx of new apps that came with it.</li>
<li><strong>Price</strong>: Cheaper subscriptions are 7.3 points more common among high retainers. AI apps in the lowest price band make up 15.6% of the high-retention group but only 8.3% of the low-retention group. One possible interpretation is that proving the value of AI features becomes harder as prices increase.</li>
<li><strong>Size</strong>: Small apps (under 500 paid subscriptions in the cohort) are 6.4 points more common in the low-retention group. But it isn't decisive: 44% of high-retention apps had fewer than 500 paid subscriptions in the retention cohort, and small apps are actually most common in the mid-retention group.</li>
<li><strong>Category</strong>: Utilities and Education lean toward high retention; Photo &amp; Video and Media &amp; Entertainment lean toward low. The likely reason is deeper than the category itself: how naturally the use case turns into a habit.</li>
</ul>
<p>These are patterns, not proof of causation. Some may simply be consequences of stronger retention: an app that retains well, for example, may have more room to offer lower prices or generous trials.</p>
<p>But the low-retention profile is still striking: newer apps, no trials, weekly plans, and higher prices. It looks a lot like the playbook for turning a spike in attention into revenue quickly, without necessarily giving users a reason to stick around.</p>
<blockquote><p>&quot;Teams building AI-first apps often focus on improving conversion in onboarding and finding the perfect price, rather than on retention. Many of these apps are barely used long-term, because users came from a trending video showing one specific feature.&quot;</p><footer>David Vargas - App Growth Consultant</footer></blockquote>
<h2>How to think about retention for your AI app</h2>
<p>These findings don't provide a single formula for retention. Instead, they show what better-retaining AI apps have in common and where product and monetization choices may make a difference.</p>
<p>Three areas are worth focusing on: getting subscribers to a useful result fast, matching your trial to how quickly your product delivers value, and choosing a monetization model that fits your costs.</p>
<h3>1. Make value land before the first renewal</h3>
<p>AI apps often have a one-and-done problem. Many subscribers come for one impressive result — a selfie turned into a professional headshot, a room redesigned in one tap — get it, share it, and have no reason to come back.</p>
<p>The challenge is turning that first result into recurring value. Start with two questions:</p>
<ul>
<li>How quickly subscribers reach a meaningful result</li>
<li>What gives them a reason to return before renewal</li>
</ul>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/f1c51114e3e6adccfe3cc22cf76bb8e73355e374-1392x937.png" alt="How to boost retention"/></figure>
<p>On the first, AI apps have an unusual advantage: they can generate a personalized result from the subscriber’s own photos, voice, or data within minutes. Design onboarding to get users to that first useful output as quickly as possible.</p>
<p>The second is harder: an impressive first result isn't always enough to bring users back. So you need to build reasons to return to the product:</p>
<ul>
<li>Outputs that improve as the model learns the subscriber</li>
<li>Work that accumulates over time (projects, history, a library)</li>
<li>External triggers like widgets or reminders</li>
</ul>
<blockquote><p>&quot;The AI apps that retain do a lot of lifecycle marketing: email, push, special offers for existing users. And above all, organic content — users who enter through that channel arrive warm and already connected to the brand.&quot;</p><footer>David Vargas - App Growth Consultant</footer></blockquote>
<p><strong>→ Your next step:</strong> Learn <a href="https://www.revenuecat.com/blog/growth/how-subscription-apps-can-become-painkillers/">how subscription apps can become painkillers</a> and <a href="https://www.revenuecat.com/blog/growth/solve-app-problems-emotionally/">how emotional value can strengthen retention</a>.</p>
<h3>2. Align your trial with your product's value cycle</h3>
<p>Our data show that 7-day trials are more common among high-retention AI apps, whereas no trial is more common among low-retention apps.</p>
<p>But it also varies by subscription duration: offering trials is associated with better retention on weekly and monthly plans, while annual plans show the opposite pattern.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ffc2d268d4b24b56c204056adb6948ecf8f38edc-1921x1080.png" alt="offering trials is associated with better retention on weekly and monthly plans, while annual plans show the opposite pattern among AI apps."/></figure>
<p>To find the right solution for your app, look at how your product delivers value:</p>
<ul>
<li>How quickly can someone reach a useful outcome?</li>
<li>How many sessions does it take?</li>
<li>Does the value improve through personalization, repeated input, or habit formation?</li>
<li>What needs to happen before paying feels reasonable?</li>
</ul>
<p>AI apps have another constraint: every generation costs money. If a longer trial gets expensive, limiting the number of generations can control costs while still giving users enough time to experience the product and come back.</p>
<p><strong>→ Your next step:</strong> Read <a href="https://www.revenuecat.com/blog/growth/7-day-trial-subscription-app/">The 7-day trial, and other free trial myths</a> to understand how to define a trial length around your product’s value cycle. Then, calculate the <a href="https://www.revenuecat.com/blog/growth/ai-feature-cost-subscription-app-margins/">true cost of your AI features</a>.</p>
<h3>3. Optimize your monetization model</h3>
<p>Subscription-only apps are more common among high retainers in our dataset, but that doesn't mean every AI app should rely on subscriptions alone. A quarter of the strongest-retaining AI apps also sell consumables.</p>
<p>For AI apps, the right model depends partly on how usage drives costs. A hybrid model can balance this by combining a subscription with extra charges for additional generations, credits, or premium features.</p>
<p>It also depends on the product itself and how users experience its core value.</p>
<blockquote><p>“Fitness and education apps can often justify free trials because they’re closely tied to activation and long-term retention. Many AI apps have a different dynamic: inference costs make trials more expensive, while the core value is often much faster to experience. Generating a video or an image typically requires less commitment than completing a workout or a lesson, so the optimization focus shifts toward maximizing upper-funnel conversion and monetizing access to core actions, for example, charging for additional generations or premium capabilities.”</p><footer>Cristian Rotari - Product Growth Consultant</footer></blockquote>
<p>Whatever model you choose, evaluate its impact over multiple renewal cycles. A monetization change can increase short-term revenue while weakening long-term retention.</p>
<p><strong>→ Your next step:</strong> Learn <a href="https://www.revenuecat.com/blog/growth/ai-hybrid-monetization/">why hybrid monetization is becoming the default model for AI subscription apps</a> and see <a href="https://www.revenuecat.com/blog/growth/ai-subscription-app-pricing">how AI apps are adapting their pricing models</a>.</p>
<aside class="tip"><strong>Measure the long-term impact of monetization changes. </strong><p><a href="https://www.revenuecat.com/feature/charts">RevenueCat Charts</a> helps you track retention, LTV, churn, and revenue across subscriber cohorts.</p></aside>
<h2>What’s next</h2>
<p>This research shows that AI apps can both convert better than traditional subscription apps and achieve comparable long-term retention.</p>
<p>But we also want to dig even deeper into <em>how</em> they do it.</p>
<p>As our next step, we’ll talk to the teams behind some of the highest-retaining AI apps to understand what they're doing differently. We'll share what we learn along the way.</p>
<h2>How we grouped and analyzed the AI apps</h2>
<p>Because an annual subscriber renews once a year while a weekly subscriber renews 52 times, retention was compared within plan length: an app's weekly subscriptions were measured against other apps' weekly subscriptions, monthly against monthly, and annual against annual. Apps selling several plan lengths get one combined score across their qualifying durations.</p>
<p>We then split them into three groups:</p>
<ul>
<li>Low-retention group: The bottom 30% of the retention score (1,056 apps)</li>
<li>Mid-retention group: The middle 40% (1,407 apps)</li>
<li>High-retention group: The top 30% (1,056 apps)</li>
</ul>
<p>Retention is based on paid subscriptions started between July 2024 and June 2025, tracked for a full year. That includes both direct purchases and converted trials.</p>
<ul>
<li>A subscription counts as retained if it completed every renewal in that year: one for annual plans, 12 for monthly, 52 for weekly.</li>
<li>We measure at the subscription level, so a subscriber who switches to a different plan counts as a churned subscription.</li>
</ul>
<p>We also verified that the trait patterns (trials, monetization, price, launch year) hold when comparing high- vs. low-retention apps within the six biggest categories. Health &amp; Fitness is the one exception: there, trials and monetization barely separate high from low retainers. One plausible reason is that the habit is built into the use case itself.</p>
<p>Scope of the data:</p>
<ul>
<li>3,519 AI app configurations (2,633 iOS, 886 Android)</li>
<li>'AI-powered' is a classification, and it spans very different products, from fitness coaches to video editors. Ranking apps only against the same plan length and checking the findings within categories addresses part of that mix, and the groups stay deliberately broad</li>
<li>Paid subscriptions started between July 2024 and June 2025, including both direct purchases and converted trials</li>
<li>Over 50 million paid subscriptions in the retention cohort</li>
<li>11 app categories, from Productivity to Photo &amp; Video, across 7 global regions</li>
<li>Every app has at least 100 eligible subscriptions, so no group is built on tiny samples</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How a $100 ad budget and a MacStories review turned Sequel into a hit]]></title>
      <link>https://www.revenuecat.com/blog/growth/romain-lefebvre-sequel-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/romain-lefebvre-sequel-launched-podcast-2026</guid>
      <pubDate>Wed, 12 Aug 2026 13:56:38 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Romain Lefebvre spent $100 on ads, refused to hustle on social media, and still built the most recommended media tracker on the internet — because he designed the app to market itself.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/7e5acd851591803be3ab3eec8252f2cbce064d7a-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Word of mouth is usually treated as a happy accident. For Romain Lefebvre, it was a core design requirement.</p>
<p><a href="https://www.youtube.com/watch?v=TTLmH1TiKbs">Watch on YouTube</a></p>
<p>When he started building Sequel — an app to track movies, TV shows, games, and books in one place — he knew he didn't want to spend his days hustling on social media or managing ad campaigns. In fact, he spent a total of $100 on Apple Search Ads before deciding it wasn't for him. Instead, he engineered the app to market itself.</p>
<p>The media detail screen in Sequel isn't just optimized for the person using the app. It's optimized for the moment that person pulls out their phone to recommend a movie to a friend. By prioritizing large, beautiful artwork over dense text, the screen acts as a billboard. &quot;If it looks sleek and polished, I wanted the other person to go like, 'Wow, what is this?'&quot; Romain says.</p>
<p>That single design decision turned his existing users into his most effective acquisition channel.</p>
<h2><strong>The $1,000 reality check</strong></h2>
<p>The indie developer journey is rarely an overnight success, and Romain's path was no exception. After volunteering for redundancy at his startup job to go full-time on Sequel, the initial financial reality was grim. In the first eight months after launch, the app made just $1,000.</p>
<p>Instead of endlessly grinding without a safety net, he set a hard deadline. He decided that if the upcoming version 2.0 update didn't generate £1,000 in its first month, he would stop and look for a job. Setting that milestone removed the emotional weight of slow growth. It turned a vague period of uncertainty into a clear, binary decision point.</p>
<h2><strong>The MacStories effect</strong></h2>
<p>The turning point for Sequel didn't come from an App Store feature. It came from the Apple enthusiast community.</p>
<p>Romain had quietly reached out to a few Mac and app-focused websites, resulting in a small newsletter mention that brought in his first wave of real user feedback. But it was the launch of version 2.0 that changed everything. The team at MacStories wrote a full, deep-dive review of the update.</p>
<p>&quot;They wrote about my product better than I could have ever done myself,&quot; Romain recalls. They highlighted the exact design details and interactions he was most proud of. That review put Sequel in front of the tastemakers. Shortly after, YouTuber Quinn Nelson (Snazzy Labs) and The Verge's David Pierce both independently noted that Sequel was the most recommended app in their audience replies. The word-of-mouth engine Romain had designed was finally running at scale.</p>
<h2><strong>Why a capped free tier is really a trial</strong></h2>
<p>One of the hardest decisions in building a freemium app is figuring out what goes behind the paywall. Romain's initial strategy for Sequel was to limit free users to tracking 25 items total. He quickly realized this was a mistake.</p>
<p>As host Charlie Chapman points out in the episode, an app enthusiast who knows they will eventually hit a hard limit stops treating the app as free software. They mentally convert it into a trial. From the first open, they are hunting for the paywall to decide if the eventual cost is worth their time.</p>
<p>Romain eventually pivoted to an unlimited free tier, moving specific features like release notifications behind the paywall instead. The lesson? It's much easier to turn a paid feature into a free one than to take a free feature away from your users.</p>
<p>In <a href="https://www.youtube.com/watch?v=TTLmH1TiKbs" target="_blank" rel="noopener noreferrer">the full episode</a>, Romain also talks about how he accidentally became a designer after being hired as an engineer, the existential risk of relying on free third-party APIs, and why writing down your design principles makes it much easier to say no to feature bloat.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://www.threads.net/@roman_lfb">Romain Lefebvre on Threads</a></li>
<li><a href="https://mastodon.social/@rlfb">Romain Lefebvre on Mastodon</a></li>
<li><a href="https://apps.apple.com/us/app/sequel-media-tracker/id1630746993">Sequel on the App Store</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[RevenueCat and AB180 partner to support South Korea’s app community]]></title>
      <link>https://www.revenuecat.com/blog/company/revenuecat-korea-partnership</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/revenuecat-korea-partnership</guid>
      <pubDate>Tue, 11 Aug 2026 08:47:20 GMT</pubDate>
      <dc:creator><![CDATA[Lauren Helstab]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[South Korean app users are among the highest spenders — and now South Korean developers have dedicated help to capture that revenue.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/fb876be65de25bff35de586636c31070f49b4a0f-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>South Korean app users are among the highest-spending mobile users in the world, with the average person <a href="https://www.google.com/url?q=https://www.businessofapps.com/data/south-korea-app-market/&amp;sa=D&amp;source=docs&amp;ust=1786527396341915&amp;usg=AOvVaw0F4t-_bmSKgMrKnYCU8nDj">spending $143 per year </a>on apps. Now, developers in South Korea have dedicated support to capture more of that — more revenue, more retained subscribers, faster growth.</p>
<p><a href="http://revenuecat.com/ab180">We've partnered with AB180</a> to give South Korean developers hands-on local support and help getting the most out of RevenueCat's tools, backed by a team that already works with some of South Korea's biggest apps and games like NAVER WEBTOON, MUSINSA, Amorepacific, and Nexon.</p>
<p>AB180 is a South Korea-based marketing technology company with over 10 years’ experience in MarTech and AdTech, and the team behind the Airbridge attribution platform. The partnership makes it easier for South Korean app developers to get local support, expert guidance, and access to world-class tools for managing and optimizing in-app subscriptions.</p>
<h2>Local expertise, backed by a global platform</h2>
<p>AB180 will support all RevenueCat customers in South Korea, from first integration through to scaling monetization, at no extra cost. It's included for every RevenueCat customer in the region.</p>
<p>If you're building in South Korea, here's what AB180 can help with:</p>
<ul>
<li>Setting up and integrating RevenueCat across iOS, Android, and the Web</li>
<li>Moving over from another provider or your own in-house system</li>
<li>Digging into your performance data to understand what's working and where you stand against benchmarks</li>
<li>Rolling out RevenueCat's growth tools: paywall builder, web-to-app funnels, and experiments</li>
<li>Joining a growing network of local developers building and scaling subscription apps</li>
</ul>
<p>With this step, RevenueCat is continuing to expand its <a href="https://www.revenuecat.com/blog/company/revenuecat-turkiye-partnership/">global partner network</a> and empowering developers everywhere to monetize smarter, scale faster, and focus on building apps users love.</p>
<h2><strong>Ready to grow?</strong></h2>
<p>Our partnership with AB180 is one more step in our mission to help app developers worldwide make more money. If you're a South Korea-based app builder, <a href="http://revenuecat.com/ab180">get in touch with AB180 through our form here</a> to see how they can support your growth journey.</p>
<p>You can also check out our <a href="https://www.revenuecat.com/docs">Docs</a> for detailed information on getting started and growing with RevenueCat.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[His crowdfunding round was oversubscribed in 24 hours. He shut it down at 3x the target.]]></title>
      <link>https://www.revenuecat.com/blog/growth/jelte-liebrand-savvy-navvy-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/jelte-liebrand-savvy-navvy-sub-club-podcast-2026</guid>
      <pubDate>Wed, 05 Aug 2026 09:36:33 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Jelte Liebrand walked away from a VC term sheet, then turned his app's own users into 2,500 investors — and his first piece of advice to founders considering the same path is "don't raise at all."]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/ab9f80564c2677c409bbd2d092ff4f52686e94f0-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>When Savvy Navvy — the &quot;Google Maps for boats&quot; — needed capital to push into marketing, founder Jelte Liebrand had already been down the standard path. A Google pitch event led to VC conversations, flights, and a term sheet. But the friction was there before the ink: &quot;They want one thing from the business, we want something else.&quot; Both sides walked away.</p>
<p><a href="https://www.youtube.com/watch?v=sKonOtcJFTU">Watch on YouTube</a></p>
<p>A year later, an angel-turned-customer asked a question that changed the company's trajectory: have you considered crowdfunding? With 10,000 boaters already using the app, Liebrand launched an equity crowdfunding campaign with a target of £125,000 and no idea what to expect.</p>
<p>&quot;Within 24 hours we're oversubscribed,&quot; he says. &quot;Within six days we shut it down because it was triple of what we needed.&quot;</p>
<p>Savvy Navvy has repeated the playbook several times since, with most rounds landing around the £1 million mark: two or three angels at six figures each, then a long tail of customers investing anywhere from $10 to hundreds of thousands. The result is 2,500 investors who don't just fund the business — they answer logistics questions, make marketing introductions, and jump on calls. &quot;We basically have this army of investors who A, believe in what we do, B, can back us with money, but also back us with knowledge.&quot;</p>
<h2><strong>&quot;It's not some magic money tree. You're selling your business.&quot;</strong></h2>
<p>For all the success of the crowdfunding rounds, Liebrand is blunt about fundraising itself. &quot;I actually hate the phrase raising money,&quot; he says. &quot;It's not some magic money tree that you put some water in and you're raising this magical free money. You're selling your business.&quot;</p>
<p>His advice to founders who ask him about crowdfunding is even blunter: &quot;My first bit of advice is don't raise at all. If you can get away with not raising VC or crowd or angel or anything else, just don't.&quot; And for those who do: raise half of what you were planning to.</p>
<p>The numbers behind Savvy Navvy's rounds are public on the platform, and they're a useful reality check. The last round valued the company at £15 million against roughly $3.5 million in B2C ARR plus a couple million more in B2B revenue — a 3–4x multiple, not the 20x founders might fantasize about. As David points out in the episode, apps are typically acquired at around 4x trailing profit, which makes a 3–5x revenue multiple on a consumer app healthy, not stingy.</p>
<h2><strong>The 2-year subscription that transformed CAC payback</strong></h2>
<p>Asked for his biggest win of the past year, Liebrand doesn't hesitate: two-year subscriptions.</p>
<p>The mechanics are simple. Savvy Navvy's standard price is $129 per year; the two-year plan is $183 — roughly 30% off. Boat owners keep their boats for years, so the longer commitment fits how customers actually use the product. But the real payoff is timing: &quot;The benefit for us isn't actually, ooh, that's more money. It's more money upfront... that's had a huge impact on what we're then able to do on the marketing side and the spend that we can have, because we get that back immediately as we spend it.&quot;</p>
<p>If annual plans were the industry's answer to CAC payback, this is the same logic taken one step further — two years of guaranteed LTV, collected on day one.</p>
<h2><strong>The signup experiment that left scars</strong></h2>
<p>Liebrand's biggest fail of the year is one many growth teams will recognize. Account creation is a friction point, so the team tested removing it entirely — &quot;anonymous accounts&quot; created silently under the hood.</p>
<p>&quot;Initially the results were through the roof. It was amazing,&quot; he says. Then they rolled it out to 100%, and the success rate started to drop. Users who claimed to want privacy still wanted to sync between phone and iPad — which requires an account. Support tickets piled up. And metric issues meant the original uplift was overstated anyway. &quot;Internally people have scars from it.&quot;</p>
<p>His broader warning: without a billion users, most startups don't have the sample size to A/B test properly, and it's dangerously easy to read into the numbers what you want to see while something further down the funnel quietly breaks.</p>
<h2><strong>Word of mouth, but not every mouth is equal</strong></h2>
<p>Savvy Navvy runs a large program for boating instructors — no affiliate codes, no kickbacks. Many instructors explicitly don't want them, because they don't want to be seen as sales reps. Instead, they get free access, purpose-built teaching tools, and first crack at beta features.</p>
<p>Liebrand explains the logic with a story from his own training: an instructor teaching him to keep his thumbs clear of a winch — while missing a thumb from doing exactly that. &quot;I'm going to trust anything that guy says,&quot; he laughs. &quot;Word of mouth is a great thing for any business, but not every mouth is the same value.&quot;</p>
<p>The same thinking powers Savvy Navvy's B2B flywheel: partnerships with boat manufacturers, starting with electric boat maker Arc Boats, now put the app directly on helm displays — and an announcement timed to this episode brings CarPlay-style navigation to pontoon boats. When a manufacturer that has been building boats for decades ships your app on the dash, that's validation no ad budget can buy.</p>
<p>In <a href="https://www.youtube.com/watch?v=sKonOtcJFTU" target="_blank" rel="noopener noreferrer">the full episode</a>, Jelte and David also cover the clipboard-powered user research that revealed the real market, why running freemium in the US but not elsewhere gives the company two business models, and why &quot;if your product comes with a manual, you've already lost.&quot; Jelte also joins the Sub Club YouTube livestream on August 6th at 9:00 AM Pacific / 18:00 CET to take listener questions.</p>
<h2>Guest links</h2>
<p>•<a href="https://uk.linkedin.com/in/jelte-liebrand">Jelte Liebrand on LinkedIn</a></p>
<p>•<a href="https://www.savvy-navvy.com/">Savvy Navvy</a></p>
<p>
</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA["What do you want me for then?" The icon designer's dilemma in the age of AI]]></title>
      <link>https://www.revenuecat.com/blog/growth/matthew-skiles-icon-visual-designer-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/matthew-skiles-icon-visual-designer-launched-podcast-2026</guid>
      <pubDate>Wed, 29 Jul 2026 12:38:31 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Matthew Skiles has spent 15 years hand-crafting the app icons you see all over WWDC videos — and he got his start with no portfolio, no formal training, and a lucky break.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/0550d6537a889bf30fe31650b0ae562c040ef48a-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Matthew Skiles is the designer behind an absurd number of indie app icons — including the Launched airplane itself, which he redrew when Charlie rebooted the show. Go to<a href="https://matthewskiles.com"> matthewskiles.com</a> and start scrolling, and you'll recognize icon after icon. His work shows up on the wall of icons in WWDC videos, and last year the Tide Guide icon he designed was held up on camera in Apple's WWDC rap video.</p>
<p><a href="https://www.youtube.com/watch?v=4GWNH4o91Mw">Watch on YouTube</a></p>
<p>None of it started with credentials. Skiles is a self-taught designer from the South Island of New Zealand who built his first website at 15 from an HTML book, coding a one-pager for a tennis coach with his brother. His first app icon was for Rock'n Rockets, an iPhone game the two brothers shipped themselves — covered twice on MacStories, played by maybe a hundred people.</p>
<p>When paid icon work finally arrived around 2014, it didn't come through a portfolio, because he didn't have one. &quot;My initial app icon work was people I already knew from other projects,&quot; he says. &quot;I didn't have the portfolio of any kind to show anyone that I can do app icons, but they knew my work already and they had enough faith in me to give it a shot.&quot; From there, the strategy was simple and stubborn: only post the icon work, never the web work, first on Dribbble and later on Twitter — until Christian Selig commissioned alternate icons for Apollo and put his name in front of a massive audience.</p>
<h2><strong>&quot;If I was a smart businessman, I wouldn't have made that decision&quot;</strong></h2>
<p>App icons are just about the worst consulting business you can pick: short engagements, little repeat work, a constant need for the next client. Charlie asked how he squares that, and Skiles didn't pretend there was a clever strategy.</p>
<p>&quot;If I was a smart businessman, I wouldn't have made that decision, but I make decisions based on it's fun,&quot; he says. &quot;It was not a smart business decision. I'm still not sure if it's a smart business decision, but it keeps working, so I'll keep doing it.&quot;</p>
<p>What makes it keep working is the scale of the App Store itself. After a career's worth of icons, he's still surprised: &quot;How can there be more apps and more people that I haven't worked with? But there's always a new person who comes along and says, 'I've got a new app and I want you to make an app icon.'&quot; It's a job that didn't exist when he was a kid — and it exists now because Apple built an ecosystem where developers are expected to care about how their icon looks.</p>
<h2><strong>The most common icon mistake: your symbol is too big</strong></h2>
<p>Skiles's process is more analog than you'd guess. He sketches roughly ten concepts in pencil on paper printed with half-icon grids, builds the winning shape in Illustrator, refines in Sketch, and only reaches for Photoshop when something needs texture — &quot;there's no better tool for building textured things than Photoshop,&quot; whatever people say about those apps being dead. Color comes last, only after the shape is settled.</p>
<p>His philosophy runs against the obvious instinct. The best icons, he argues, aren't literal representations of the app — think Transmit's truck or Coda's leaf. &quot;The app icon will make them be like, 'I'll give it a shot because I want that in my dock.'&quot; A utility you use every day should be something you like having around, the same way you'd rather own a nice pen than an ugly one.</p>
<p>And for anyone designing their own icon, he offers the single most common fix: shrink your symbol. People scale their logo up until it fills the squircle, and it almost never looks right. &quot;Just give it a bit of breathing room. Just shrink it down just a little bit and it'll look nicer.&quot; Even Apple's own grid circle is often too generous — &quot;you actually want to be a little smaller than that.&quot;</p>
<h2><strong>AI hasn't replaced him — but it has created a strange new client problem</strong></h2>
<p>Skiles doesn't use AI in his own work (&quot;I love the process of doing it myself&quot;), but it's already changed his client relationships in two opposite ways.</p>
<p><strong>The good:</strong> AI images are a genuinely useful communication tool. When a client arrives with a generated reference, &quot;it gives them a way to convey that to me, whereas it would've taken a lot longer to find what it is they're looking for.&quot;</p>
<p><strong>The bad:</strong> infinite generation creates choice paralysis. Clients show up with AI-generated options, he recommends a direction, and &quot;then they will go and generate 50 more options... at some point you have to make a choice so I can go in that direction with you.&quot; Worse is when a client falls in love with a specific AI image and just wants it reproduced: &quot;You're almost stuck trying to replicate the AI design for people, and you're like, 'What do you want me for then, if you just want me to redraw that thing for you?'&quot;</p>
<p>His overall stance is deliberate patience — the same wait-and-see approach he once took with Sketch before it won him over. Tools get easier to adopt over time, so there's little risk in watching first. The one area pulling him in: custom generators for textures and assets, where the designer keeps control over what's being generated.</p>
<h2><strong>He has thoughts about Liquid Glass</strong></h2>
<p>Skiles lived through the great flattening of iOS 7 — which, he says, made him better, because &quot;before that you had details and textures to hide behind... you just had to have a great shape.&quot; He loved the Big Sur era, where icons fit the squircle by convention but could still break out of it: &quot;an advised constraint versus a forced constraint, I think, gives the best results.&quot;</p>
<p>Tahoe's mandatory glass frame is another matter. Icon Composer saves him real time when a client wants a system-native look across platforms, but the non-negotiable edge highlight grates: &quot;If it was up to me, that effect wouldn't exist. Let people put glass within the icon, but just don't put it on the frame... But this is the world we live in. We accept what Apple gives us and we find a way to make it work.&quot;</p>
<p>In <a href="https://www.youtube.com/watch?v=4GWNH4o91Mw" target="_blank" rel="noopener noreferrer">the full episode</a>, Matthew also talks about the lost golden age of the Dribbble design community, why he almost never gets Android icon work, the current trend of custom glass shaders and mesh gradients, and the delicious-era Mac designers — Jasper Hauser's AppZapper ray gun among them — that he's still chasing: &quot;When I was a kid, that was my dream, and it's still my dream now.&quot;</p>
<h2>Guest links:</h2>
<ul>
<li><a href="https://matthewskiles.com/">Matthew Skiles's portfolio</a></li>
<li><a href="https://x.com/matthewskiles">Matthew Skiles on X</a></li>
<li>Matthew is also on Mastodon and Bluesky as @matthewskiles</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[A user costs $30 and returns $50. Yuliya Lennox says that’s bad.]]></title>
      <link>https://www.revenuecat.com/blog/growth/yuliya-lennox-solid-starts-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/yuliya-lennox-solid-starts-sub-club-podcast-2026</guid>
      <pubDate>Wed, 22 Jul 2026 13:07:46 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Yuliya Lennox spent a decade scaling apps—and learned that a campaign making $20 per acquired user can be the clearest sign its creative is not good enough.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/89e692fc12ad727ffb875c9626f2e860c80e798c-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<h2><strong>A $20 margin can hide mediocre creative</strong></h2>
<p>When an app starts spending its first $100 or $500 a day on performance marketing, a stable return feels like success. A user costs $30 to acquire and returns $50. The campaign is profitable. Why not scale it?</p>
<p>Yuliya argues that this is exactly when a team should become suspicious.</p>
<p>“This is bad for you actually because you stop trying and you think, oh, my LTV ROI, whatever, it looks great. I'm doing everything fine. You are not doing fine. You are doing very, very badly.”</p>
<p><a href="https://www.youtube.com/watch?v=jzlPf100vy4">Watch on YouTube</a></p>
<p>A predictable margin can convince a team that it has solved acquisition before it has found a genuinely exceptional creative. The stronger signal is not steady performance across several ads. It is the outlier that changes the curve: CPA falls sharply, or purchases triple in a day.</p>
<p>That is the difference between an ad that backs out and an idea that can move the business. Early performance marketing should search for those step changes, not settle into comfortable averages.</p>
<h2><strong>BetterMe’s ugliest ad was the one David remembered</strong></h2>
<p>David Barnard still remembers a BetterMe ad he saw roughly a decade ago. It featured a deliberately unpleasant illustration of belly fat hanging over the front of someone’s body. It was not aspirational fitness advertising. It was visually jarring—and that is why it stayed in his memory.</p>
<p>Yuliya calls it “the best ad ever.” The polished images of slim people exercising disappeared into the category. The ugly illustration broke the pattern of the feed.</p>
<p>“They want something that catches them on the first glance.” — Yuliya Lennox</p>
<p>For an app selling a function, the first job of an ad is not to look like a campaign from an established consumer brand. It is to make the product and the pain it solves obvious in the first second. Brand polish is valuable only after the viewer understands what is being sold.</p>
<p>There is still a limit. At Solid Starts, a baby-feeding brand built around trusted expert advice, the team tested a creative featuring babies made out of fruit. The image attracted attention, but it also prompted parents to question whether they should trust the company feeding their children.</p>
<p>The lesson is not that every app should make every ad ugly. It is that most teams invoke “the brand” too early. Performance creative should have to prove that it damages trust before polish becomes the default constraint.</p>
<h2><strong>The No. 2 language app kept its founder in every marketing meeting</strong></h2>
<p>Founders often look for a senior marketer, agency, or CMO who can take growth off their plate. Yuliya’s experience suggests that delegation works better after the founder has developed their own feel for the market.</p>
<p>She points to Eva Learn English, which reached the No. 2 position behind Duolingo. Its founder attended every marketing meeting, developed creative concepts, and stayed close to how the product was communicated.</p>
<p>“It's not hard at all to understand marketing, but to feel it with your own hands, this is something that every founder needs to know.” — Yuliya Lennox</p>
<p>A founder does not need to remain the company’s permanent media buyer. But they do need enough firsthand experience to recognize a strong message, challenge a weak assumption, and know whether the team understands why customers buy. An agency can execute the work; it cannot manufacture that intuition on the founder’s behalf.</p>
<h2><strong>Sell the idea before you vibe code the app</strong></h2>
<p>Cheaper development has made it easier to build a complete product before proving anyone wants it. Yuliya recommends reversing that order.</p>
<p>“Just start with marketing, sell a PDF, sell an idea, sell something and then refund because you don't have a product.” — Yuliya Lennox</p>
<p>The tactic is intentionally uncomfortable. Put the promise in front of a real audience, ask people to pay, and learn whether demand exists before investing months in development. If the product does not exist yet, refund the buyers. The point is not to collect revenue; it is to replace encouragement from friends and family with evidence from the market.</p>
<p>This also forces a founder to define the business they are trying to build. A niche app might be perfectly capable of supporting a $20,000-a-month lifestyle business while remaining unsuitable for venture-scale economics. Marketing the idea early exposes that distinction before the cost base is locked in.</p>
<h2><strong>A $10 test market can save a $1,000 US experiment</strong></h2>
<p>Localization is usually treated as a revenue-expansion project. Yuliya also sees it as a way to make experimentation cheaper.</p>
<p>AI can translate a service in hours, making it practical to test more countries without a long localization project. Teams can use markets with lower CPMs and similar conversion behavior to learn which creative concepts deserve a more expensive US launch.</p>
<p>“You need to test 500 ads per week. And if you do that on US CPMs, you will definitely go broke.” — Yuliya Lennox</p>
<p>Yuliya says a comparable market such as Indonesia can sometimes turn a 1,000−a−day US test into a 10-a-day experiment, giving a team directional evidence before it pays US CPMs.</p>
<p>Cheap translation is not the same as local understanding. Replika made a serious push into Japan and even had a Japanese speaker on the team, but the localization still failed to gain traction. The practical play is to use international markets to widen the testing surface while recognizing that language alone cannot reproduce cultural context.</p>
<p>In <a href="https://www.youtube.com/watch?v=jzlPf100vy4">the full episode</a>, Yuliya and David also discuss how organic growth can become a trap, why subscription optimization can slide into black-hat behavior, and why running three apps at once prevents the war-room focus required to scale.</p>
<h2><strong>Guest links</strong></h2>
<ul>
<li><a href="https://www.linkedin.com/in/yulichkus">Yuliya Lennox on LinkedIn</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[A license to bill: Selling a Mac app without a merchant of record]]></title>
      <link>https://www.revenuecat.com/blog/engineering/license-mac-app-merchant-of-record</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/license-mac-app-merchant-of-record</guid>
      <pubDate>Mon, 20 Jul 2026 14:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Jared Sorge, Dave DeLong]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[How Arborist sells outside the Mac App Store using RevenueCat and Stripe Managed Payments]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/635072d674724ef02bca1a2e23c1d16d6595a2a3-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>I hang out in a couple of online communities focused on app development. One of my favorites is a Slack community called &quot;AppKit Abusers.&quot; It's a great place for developers writing Mac apps to hang out, help each other work through problems, and share ideas. There's one question that gets asked regularly:</p>
<p>How do I make money?</p>
<p>This question comes up because many Mac developers choose to distribute their apps outside the Mac App Store. Sandboxing imposes restrictions on iOS apps, but it's even more extreme for Mac apps. Many of the things we've come to expect from &quot;<a href="https://daringfireball.net/linked/2020/03/20/mac-assed-mac-apps">Mac-assed Mac apps</a>&quot; over the years are simply not possible with sandboxing. However, disabling the sandbox on a Mac app comes at a cost: you cannot ship your app on Apple's App Store. This means that all of the work of collecting money from customers, handling licensing, etc. falls squarely on the developer's shoulders. And so every couple of months, like clockwork, another developer is in the Slack group asking for recommendations on how to implement licensing.</p>
<p>This is a problem I've faced several times myself. I love writing Mac apps; I've got three I'm building for myself right now. On iOS, I've used RevenueCat to simplify the logic around dealing with in-app purchases, and it's been wonderful to offload that mental burden onto a reliable framework, and allow me to focus on the task of <em>building my app</em>. I've wished something as easy existed for Mac apps, too.</p>
<p>Earlier this year, I joined RevenueCat as a Senior SDK Engineer and learned more about all the ways they help developers make money. Of course, I immediately thought &quot;Well, what about Mac developers?&quot; One feature in particular caught my eye: <a href="https://www.revenuecat.com/feature/web">web-based billing support</a>. This is a way for web developers (and mobile developers selling outside their platform stores) to offer subscriptions to their customers. Since all the information is associated with RevenueCat's service, purchases made on the web get tied to the customer’s profile, which can then be accessed via the RevenueCat SDK in an app. And since the RevenueCat SDK works on macOS… I quickly started connecting the dots and forming a plan.</p>
<p>After thinking about this approach, I put it on my to-do list to see if it would actually work. As luck would have it, a couple of days later my friend <a href="https://jsorge.net">Jared Sorge</a> mentioned he was looking to solve this exact problem, and he was game to try my idea. After some careful reading of the documentation, a couple of Zoom chats, and a bit of trial and error, Jared got it to work. In the end, it was extremely straightforward:</p>
<ul>
<li>Configure the product offerings and entitlements in the RevenueCat dashboard, like you would for any other app</li>
<li>Connect Stripe as a <a href="https://www.revenuecat.com/docs/projects/connect-a-store">web provider</a> and hook your offering up to a <a href="https://www.revenuecat.com/docs/web/web-billing/web-purchase-links">Web Purchase Link</a></li>
<li>Send identified users from your Mac app to the web to purchase</li>
<li>After purchase, ask the SDK to refresh, and see that they now have the purchased entitlement</li>
</ul>
<p>That's the short version. The longer version is Jared's story—how he got there for <a href="https://taphouse.io/arborist">Arborist</a>, his new Mac app launching today.</p>
<h2>Why not Paddle, FastSpring, or Lemon Squeezy?</h2>
<p>Arborist is a native macOS command center for git repositories and worktrees. It works by running user-entered commands at arbitrary locations, which means sandboxing—and therefore the Mac App Store—was never an option. Jared settled on a one-time version 1 purchase of $39, &quot;just like the Software Days of Yore,&quot; and then hit the question every direct-sales Mac developer hits: how do I do licensing on my own?</p>
<p>He knew the established options like <a href="https://www.paddle.com">Paddle</a>, <a href="https://fastspring.com">FastSpring</a>, and <a href="https://www.lemonsqueezy.com">Lemon Squeezy</a> have worked for many apps like his over the years. But:</p>
<p>I also wanted something simple for my users. Not to mention Apple Pay was a must-have. I've experienced friction more times than I can count when an app doesn't support Apple Pay, and every time it happens I am ever so slightly less happy with that app because of it. Customers are also more familiar with the smooth in-app purchase experiences provided by the App Store, and if I could achieve something that easy I wanted to do just that.</p>
<h2>No license server, no license keys</h2>
<p>What Jared landed on contains three pieces:</p>
<ul>
<li>RevenueCat serves as the source of truth for whether or not a customer is licensed.</li>
<li>Customer IDs come from local iCloud identifiers—specifically, calling <code>userRecordID()</code> on the app's CKContainer in CloudKit.</li>
<li>When the user makes a purchase, they earn an <a href="https://www.revenuecat.com/docs/getting-started/entitlements">entitlement</a> that Arborist looks for to grant them a license.</li>
</ul>
<p>The second bullet is the clever one:</p>
<p>I didn't want to have to spin up a licensing server to send out codes (much less have to store them), and I didn't want to have an email-based system. I wished for something simple like StoreKit but without using the App Store as a backend.</p>
<p>Because the customer ID is the user's iCloud record ID, a license bought on one Mac automatically follows the customer to every Mac signed in to the same iCloud account. There are no license keys to email, no &quot;Restore Purchases&quot; button, and no personally identifying information collected by the app. As Jared puts it: &quot;Stripe handles the payment process, and the iCloud's identifiers give nothing away.&quot;</p>
<p>The trade-off is that this strategy requires the customer to be signed in to iCloud—a reasonable assumption for a power-user app, and Jared is working on a web-based iCloud sign-in for everyone else, coming after 1.0.</p>
<h2>The merchant of record question</h2>
<p>Jared initially set up <a href="https://www.revenuecat.com/billing">RevenueCat Billing</a>, RevenueCat's own billing engine for web purchases, but then he ran into a term he'd never encountered:</p>
<p>I had never heard the term <a href="https://stripe.com/resources/more/merchant-of-record">merchant of record (MoR)</a> before and when I first did I didn't know it was something to care about (spoiler alert: it very much is).</p>
<p>The merchant of record is the entity legally responsible for the sale: collecting and remitting sales tax, VAT, and GST, and handling refunds. RevenueCat Billing does not act as your merchant of record: you are. For solo developers, this can add a significant amount of complexity. For Jared and his first direct-sale app, it was a dealbreaker.</p>
<p>The good news is that Stripe offers <a href="https://stripe.com/managed-payments">Managed Payments</a>, a merchant-of-record service where Stripe takes on tax collection and remittance per transaction, and RevenueCat <a href="https://www.revenuecat.com/docs/web/integrations/stripe/stripe-managed-payments">supports it out of the box</a>. From an app developer’s perspective, it’s the same entitlements, same SDK, and same Web Purchase Links; Stripe just carries the compliance burden. It adds a cost, but that was worth it to Jared:</p>
<p>Thankfully I found Managed Payments and from everything I can tell, making Stripe my MoR puts all the burden of these collections and distributions on Stripe. So I'm happy to give them a few extra percent of each sale to handle that for me.</p>
<h2>Setting up the backend</h2>
<p>Jared has documented his full setup, including the places he tripped up, <a href="https://jsorge.net/2026/07/15/revenuecat-mac-licensing">on his blog</a>. The condensed version is short:</p>
<ol>
<li>Create the entitlement in RevenueCat that the app checks for a license.</li>
<li>Add the product in the Stripe dashboard, making sure it meets Managed Payments' eligibility criteria.</li>
<li>Connect Stripe to RevenueCat as a <a href="https://www.revenuecat.com/docs/projects/connect-a-store">web provider</a>, checking the <a href="https://www.revenuecat.com/docs/web/integrations/stripe/stripe-managed-payments">&quot;Use Managed Payments when available&quot; box</a>.</li>
<li>Create the RevenueCat product by importing it from Stripe.</li>
<li>Add an offering containing the product with a <strong>lifetime</strong> duration—effectively a one-time purchase.</li>
<li>Generate a <a href="https://www.revenuecat.com/docs/web/web-billing/web-purchase-links">Web Purchase Link</a> for the offering, including the callback URL the web checkout uses to deep-link back into the app.</li>
</ol>
<h2>Hooking up the app</h2>
<p>The full purchase flow in Arborist looks very familiar to app users: open the License screen in the Settings, click “Buy,” complete the purchase in your browser, and automatically get returned to Arborist. That’s it.</p>
<p>Jared had initially hoped to keep everything in-app using a WKWebView, but a <a href="https://bugs.webkit.org/show_bug.cgi?id=282078">known WebKit bug</a> prevents Apple Pay from working in that context. Instead, he accepted the small friction of jumping out to the user’s browser so customers can use Apple Pay instead of typing in card details by hand.</p>
<p>Another caveat he found is that the RevenueCat SDK is geared toward apps that use StoreKit and doesn’t supply the corresponding web purchase link directly. Instead, he hard-codes the checkout URLs (production and sandbox) instead. When the user clicks buy, Jared's licensing view model assembles the checkout URL (shaped like <code>https://pay.rev.cat/{web_link_id}/{user_id}</code>) and hands it to the system browser:</p>
<pre><code class="language-swift">func startCheckout() async {
    guard isStartingCheckout == false else { return }

    isStartingCheckout = true
    actionMessage = nil
    defer { isStartingCheckout = false }

    do {
        let checkoutURL = try await licenseManager.checkoutURL()
        guard NSWorkspace.shared.open(checkoutURL) else {
            throw LicenseCheckoutOpenError.failedToOpenBrowser
        }
        actionMessage = &quot;Checkout opened in your browser.&quot;
    } catch {
        displayState = LicenseDisplayState.resolved(
            from: nil,
            previous: displayState,
            error: error
        )
    }
}</code></pre>
<p>After a successful purchase, RevenueCat's backend associates the passed-in user ID with the entitlement and calls back into the app via the registered deep link. The handler re-fetches the customer and validates the entitlement:</p>
<pre><code class="language-swift">func handleDeepLinkPurchase() async throws {
    let customerInfo = try await Purchases.shared.customerInfo(fetchPolicy: .fetchCurrent)

    guard
        let entitlement = customerInfo.entitlements[entitlementID],
        entitlement.isActive
    else {
        // Handle a missing entitlement; this means the
        // transaction did not succeed
        return
    }

    // Once we get here we have a validated customer who has
    // made a purchase and the app can be unlocked.
}</code></pre>
<p>That first line with the fetchPolicy is important:</p>
<p>The fetch policy here is critical because without specifying .fetchCurrent the SDK will return cached data, and if a customer has made a purchase that won't be reflected instantly like they'll expect. Instructing the SDK to bust out of its cached data is the thing that will make this process feel seamless to my customers (and yours!).</p>
<p>Similar code runs at app launch to validate the user against their iCloud ID, which is how a license bought on one Mac unlocks Arborist automatically on the next.</p>
<p>I was thrilled to discover that not only is using RevenueCat for desktop app licensing possible, but it's actually a straightforward setup process with a relatively small amount of in-app code. There are really only about three main areas where Jared needed to make changes: the UI for offering the purchase option to users and sending them to the web, the code to handle returning from the web, and the code that runs at startup to configure everything and check for existing purchases.</p>
<p>Having such a simple way to offer Mac app licensing to customers is <em>very</em> exciting to me. There are some ways I can see for the SDK to make this even easier, and I hope to start addressing them. If you're writing a Mac app and looking for a simple way to offer licensing to your customers, RevenueCat can help you make money.</p>
<p><a href="https://taphouse.io/arborist">Arborist launches today</a>—and Jared's full write-up, including everything he learned, is <a href="https://jsorge.net/2026/07/15/revenuecat-mac-licensing">on his blog</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[We partnered with Stripe so your agent can monetize your app]]></title>
      <link>https://www.revenuecat.com/blog/engineering/stripe-projects-cli</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/stripe-projects-cli</guid>
      <pubDate>Wed, 15 Jul 2026 17:21:52 GMT</pubDate>
      <dc:creator><![CDATA[Austin Blake]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[RevenueCat and Stripe Projects make app monetization setup as simple as one CLI command, so developers and coding agents can go from idea to payments faster.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f48d6f4d81d73a6cb04927c999e8510595f5d5a3-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Your coding agent can now set up RevenueCat, provision a project, generate API keys, and get your app ready to accept payments. The whole thing takes just one CLI command.</p>
<p>stripe projects add revenuecat/app
</p>
<p>We built this with Stripe as part of <a href="https://projects.dev/">Stripe Projects</a>.</p>
<p><a href="https://www.youtube.com/watch?v=KYUzTMqETso">Watch on YouTube</a></p>
<h2><strong>Before this, monetization setup was still manual</strong></h2>
<p>Every other piece of your stack provisions in seconds. Database? One command. Auth? One command. Hosting? Done. But <em>until now</em>, monetization required you to stop and spend an afternoon creating accounts, reading StoreKit documentation, figuring out which shared secret goes where, generating API keys, configuring sandbox environments, and setting up server-to-server notifications.</p>
<p>It takes hours. Sometimes days. And you haven't written a single line of product code yet.</p>
<p>That gap between &quot;I have a working app&quot; and &quot;I can charge for it&quot; is absurdly wide for something that should be a solved problem.</p>
<p>So we fixed it.</p>
<h2>One command, working credentials</h2>
<p>If you're starting a new project today, here's what this looks like in practice.</p>
<p>You (or your agent) run one command. A few seconds later, you have a RevenueCat account, a configured project, and working API keys written directly to your <code>.env</code> file. You stay in the terminal the entire time.</p>
<p>$ stripe projects add revenuecat/app

✓ Created RevenueCat account
✓ Created project &quot;my-app&quot;
✓ Generated API keys
✓ Wrote REVENUECAT_APP_UUID to .env
✓ Wrote REVENUECAT_DASHBOARD_URL to .env
✓ Wrote REVENUECAT_SECRET_API_KEY to .env

Ready. Run `stripe projects open revenuecat` to view your dashboard.
</p>
<p>Products, entitlements, and offerings can be configured from the dashboard later, or your agent can handle them via our <a href="https://www.revenuecat.com/docs/tools/mcp">MCP server</a>.</p>
<h2>Agents provision it automatically</h2>
<p>If you build in Cursor, Claude Code, or Warp, the provisioning step is something your agent handles between writing your UI code and wiring up the SDK. You don't type the command. You just say &quot;add monetization.&quot;</p>
<p>You: &quot;I want to add a $9.99/month subscription to this app.&quot;

Agent: [runs stripe projects add revenuecat/app]
Agent: [reads .env, configures Purchases SDK]
Agent: &quot;Done. I've added the Purchases SDK to your project and
        configured it with your API key. Want me to create a
        product and build a paywall next?&quot;
</p>
<p>The agent doesn't need special RevenueCat knowledge. It picks up the Stripe Projects CLI and the skill files that <code>stripe projects init</code> already created. Monetization becomes a routine provisioning step, same as adding a database or setting up auth.</p>
<h2>Account, project, keys, full platform</h2>
<p>When you run <code>stripe projects add revenuecat/app</code>, the CLI provisions:</p>
<ul>
<li>A RevenueCat account (or links to your existing one)</li>
<li>A new project with sandbox and production environments</li>
<li>API keys synced to your local <code>.env</code></li>
<li>Access to the full platform: paywalls, entitlements, offerings, analytics, webhooks, and our AI Toolkit for ongoing management</li>
</ul>
<p>You'll still need Apple and Google developer accounts to go live on those stores. But RevenueCat itself (the account, the project, the API keys, the entitlement configuration) provisions from the terminal. And with RevenueCat's test store, you can test monetization on a real device without those store accounts.</p>
<p>From there, you integrate the <a href="https://www.revenuecat.com/docs/getting-started/installation">Purchases SDK</a> for iOS, Android, React Native, Flutter, KMP, or web. That's the path from &quot;I have an idea&quot; to &quot;I'm making money.&quot;</p>
<h2>Try it</h2>
<ol>
<li>Install the Stripe CLI with the Projects plugin:</li>
</ol>
<p>stripe plugin install projects
</p>
<ol>
<li>Initialize your project and add RevenueCat:</li>
</ol>
<p>stripe projects init my-app
stripe projects add revenuecat/app
</p>
<ol>
<li>Your <code>REVENUECAT_SECRET_API_KEY</code> is in <code>.env</code>. Integrate the SDK and start making money.</li>
</ol>
<p>For the full lifecycle, check our <a href="https://www.revenuecat.com/docs/getting-started/stripe-projects-quickstart">documentation</a> or ask your coding agent to handle product creation, paywall configuration, and entitlement management using the <a href="https://www.revenuecat.com/blog/company/ai-toolkit/">RevenueCat AI Toolkit</a>.</p>
<p>This is a developer preview. <a href="https://projects.dev/">Give it a try</a> and tell us what you think, especially if you're building with agents.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[A 19-day ChatGPT wrapper that made $10,000 in 36 hours]]></title>
      <link>https://www.revenuecat.com/blog/growth/joe-fabisevich-plinky-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/joe-fabisevich-plinky-launched-podcast-2026</guid>
      <pubDate>Wed, 15 Jul 2026 12:52:48 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Joe Fabisevich left Twitter three days before the Elon Musk acquisition to build a link-saving app — but a scrappy side project taught him the true power of a single well-placed press email.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/7440b741dc0e9bbe6becdc1d7c5d41cd9e341a5d-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<h2>Treating a TestFlight like a production launch</h2>
<p>When Joe Fabisevich left Twitter to go indie, he didn't start by building his link-saving app, Plinky, in secret. Instead, he treated his TestFlight beta like a full production launch.</p>
<p><a href="https://www.youtube.com/watch?v=VNe6OcHlgnI">Watch on YouTube</a></p>
<p>For months, he ran a beta program that required real infrastructure. When he had to change Plinky's underlying data structure, he didn't just wipe the database—he built a complete export and import system so the 5% of users who were deeply attached to their links wouldn't lose them. &quot;It was not worth it in the sense of the value of the time that I spent on it,&quot; Joe admits. But the effort paid dividends. When the app officially launched and users inevitably asked for an export feature, the system was already built and battle-tested. Treating the beta with the care of a live product meant he already had the infrastructure—and the user trust—to scale.</p>
<h2>The $10,000 Daring Fireball email</h2>
<p>While building Plinky, Joe took a three-month detour. Six months after ChatGPT launched, OpenAI released the GPT-3.5 API. Joe and a friend saw an arbitrage opportunity: there was no official ChatGPT iOS app yet. They built Short Circuit, a barebones wrapper, in just 19 days.</p>
<p>The app's success wasn't driven by complex marketing or paid ads. It came down to one email. Joe pitched the app to John Gruber, framing it not just as another AI tool, but as a specific solution for Apple users. Gruber posted it on Daring Fireball. &quot;We started getting alerts from RevenueCat in our pocket,&quot; Joe remembers. &quot;We made like $5,000 in the first two hours.&quot; The app pulled in $10,000 in a day and a half. The lesson was immediate and clear: &quot;Definitely reach out to press, you idiot.&quot;</p>
<h2>Why freemium is a trap for indie apps</h2>
<p>When Plinky finally launched, Joe wanted to be generous. He offered a freemium tier that allowed users to save 50 links for free. His wife, a staff product marketer, warned him they would churn. She was right.</p>
<p>&quot;I went back and forth on freemium versus trial, and I settled on freemium because I want to be generous. And now I regret it,&quot; Joe says. The problem wasn't just lost revenue; it was lost data. Freemium users who churned disappeared invisibly, giving Joe fewer &quot;shots on goal&quot; to understand exactly when and why someone decided the app was worth paying for. He eventually dropped the free limit to 10 links and is now shifting toward a standard free trial model, realizing that charging users sooner is the only way to accurately measure the app's real value.</p>
<h2>The three-email formula for app sales</h2>
<p>Plinky is now priced at $39.99 a year, and Joe has found that the single best way to drive revenue is through structured email marketing to his free users. He collects emails upfront via Sign in with Apple, giving him a direct line to his audience that doesn't rely on App Store release notes.</p>
<p>His wife gave him a strict rule for sales: nobody cares if the discount is less than 50%. So he runs 50% off sales, and he never relies on just one email. The playbook is always three touches: an announcement email, a reminder a few days later to non-openers, and a final 24-hour warning. &quot;The last email usually drives the most people through the door,&quot; he explains. If the first email doesn't convert immediately, the strategy is working exactly as intended.</p>
<p>In the full episode, Joe also talks about how he organizes his launches using a &quot;lookback calendar,&quot; why he open-sourced his database library Boutique, and how he watched an AI agent completely rewrite the Short Circuit codebase in just three and a half hours.</p>
<h2>Guest links</h2>
<ul>
<li><a href="https://x.com/mergesort">Joe Fabisevich on X</a></li>
<li><a href="https://plinky.app">Plinky</a></li>
<li><a href="https://build.ms">Build.ms (AI Workshops)</a></li>
<li><a href="https://fabisevi.ch">Joe's Personal Site</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Almost everyone who pays for subscriptions is already on iOS 26]]></title>
      <link>https://www.revenuecat.com/blog/growth/minimum-ios-version</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/minimum-ios-version</guid>
      <pubDate>Tue, 14 Jul 2026 08:53:06 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[The business case for dropping support for earlier iOS versions
]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f7eda0327571229e366bee6ceaf10c26f653de4e-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Right before WWDC, Apple published its final adoption numbers for the iOS 26 cycle: <a href="https://www.macrumors.com/2026/06/09/ios-26-adoption-stats-wwdc/">79% of all iPhones, and 86% of iPhones introduced in the last four years, are running iOS 26</a>.</p>
<p>Those numbers convinced me to <a href="https://x.com/drbarnard/status/2064817113580363856">post a hunch on Twitter</a>: for most subscription apps, the share of paying users on iOS 26 is probably more like 95%. And if that’s anywhere close to true, it’s time to start thinking about iOS 26 as the minimum requirement. Adding new features faster could easily pay for the small drop in paying users.</p>
<p>App industry folks pushed back, added data, and sharpened the argument in ways I didn’t expect. So here’s the fuller case: when cutting older iOS versions makes business sense, when it doesn’t, and how to run the numbers on your own app.</p>
<h2>What the data says about who actually pays</h2>
<p>I made up the 95% number. Baran Toppare, a data scientist here at RevenueCat, came back with the receipts.</p>
<p>Across apps on RevenueCat, about 88% of paid subscribers who opened an app in the last 90 days are on iOS 26. iOS 18 still accounts for 9.4% of subscribers and 9.6% of revenue. New transactions tell the same story: 85% happen on iOS 26.</p>
<p>So, not 95%. But that platform-wide average includes massive apps like ChatGPT that pull it toward the broadest possible audience. Your app probably isn’t ChatGPT. When I checked Weather Up (my side-project weather app), roughly 94% of paying users active in the past 90 days were on iOS 26.</p>
<h2>The trade you’re actually making</h2>
<p>Raising your minimum OS costs you two things. Existing users on older versions keep the app, but frozen at the last compatible update. And new users on older versions can’t download it at all. That second one is the real cost, because it compounds: every new customer you can’t acquire is gone for good.</p>
<p>David Smith wrote <a href="https://www.david-smith.org/blog/2025/06/27/requiring-26/">the best conservative case</a> I’ve read. Nine months into the iOS 18 cycle, requiring the latest OS would have cut about 9% of Widgetsmith’s new downloads. As he put it: “If I could do something which boosted my downloads by 9% I’d be delighted”. His threshold: wait until older versions fall to about 1% of new downloads.</p>
<p>That’s a defensible line. Mine is more aggressive, and 2026 is a big part of why:</p>
<ul>
<li>Since April 28, <a href="https://developer.apple.com/news/?id=ueeok6yw">every App Store submission must be built with the iOS 26 SDK</a>. Your app already renders Liquid Glass for iOS 26 users, so supporting iOS 18 means shipping and QA-ing two design systems in one binary.</li>
<li>At WWDC26, Apple confirmed <a href="https://www.macrumors.com/2026/06/08/ios-27-supports-iphone-11-newer/">iOS 27 runs on the exact same devices as iOS 26</a> (iPhone 11 and newer). Requiring iOS 26 doesn’t strand anyone whose hardware could go further. Everyone excluded is on a 2018-or-older iPhone.</li>
<li>The cost shrinks every month. iOS 18 was down to <a href="https://telemetrydeck.com/survey/apple/iOS/majorSystemVersions/">about 10% of active devices by the end of June</a>, per TelemetryDeck. An iOS 26 minimum costs you a bit today, less in the fall, and almost nothing by next spring.</li>
</ul>
<h2>Why I required iOS 26 for Grit Method</h2>
<p>My latest side project app, <a href="https://gritmethod.app">Grit Method</a>, requires iOS 26. The UI leans heavily on Liquid Glass, and I didn’t want to check every animation and UI element against older simulators, then invent workarounds for a shrinking audience. One code path, the latest best practices, no backward-compatibility debt on day one.</p>
<p>There’s also a more practical problem: I no longer own a device running iOS 18. My whole family upgraded in the fall. Supporting an OS version you can only test in a simulator is its own quiet risk, especially when the people on it are paying you.</p>
<p>To be clear about my bias: I’m an indie moving fast, and the tradeoff is much smaller for me. Moving faster is worth more to my business than the single-digit percentage of paying users I’m giving up. The bigger your app, the more carefully you should run this math.</p>
<h2>When you shouldn’t cut older versions</h2>
<p>Some apps shouldn’t take this trade:</p>
<ul>
<li><strong>Apps where the audience skews toward older devices.</strong> Kids apps are the standout, since parents hand down old phones and buy used ones. Games with older demographics fit here too. As one reply on the thread put it, the Candy Crush crowd on iPhone 14s is still a market worth acquiring.</li>
<li><strong>Freemium apps that need reach.</strong> Free users skew toward older OS versions. If your model depends on a large free tier feeding a small paid one, cutting old versions taxes the top of your funnel hardest.</li>
<li><strong>Apps people need, not just want.</strong> If your app touches health, safety, or money, “they can buy a newer phone” isn’t an acceptable answer for the people who can’t.</li>
</ul>
<p>The biggest companies in the world land all over this spectrum, and the split tracks business model. <a href="https://faq.whatsapp.com/1150261202542208">WhatsApp still supports iOS 15.1</a>, a 2021 release, because reach is the product. Airbnb and Gmail already require iOS 18. Neither is wrong. They’re optimizing for different things.</p>
<p>One more nuance from the thread that stuck with me:</p>
<p>OS version is a proxy for device age, and device age is a proxy for purchase intent.</p>
<h2>The best counterargument</h2>
<p>Dan’s right that for some apps, maintaining backward compatibility is cheap. AI coding tools have made the #available dance easier than it used to be. If old-version support costs you almost nothing, keep it.</p>
<p>But “almost nothing” is doing a lot of work in that sentence. Two design systems isn’t nothing. An untestable device matrix isn’t nothing. And Dan’s own framing (better available and maybe buggy than not available) is a harder sell when the person hitting the bug is a paying subscriber.</p>
<h2>How to run your own numbers</h2>
<p>Don’t inherit last year’s minimum by default, and don’t inherit mine. Check four things:</p>
<p><strong>1. New users by OS version: </strong>In RevenueCat, go to the Customers tab and click on “New audience”. Add a filter for “First seen” with the last 90 days. Click the “Export all” button and then have your favorite AI tool analyze the data.For Weather Up, 83% of new users in the past 90 days were on iOS 26 or newer. </p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/231624bbd81364d3a30f183fbb77b74591ae8cbb-1776x816.png" alt=""/></figure>
<p><strong>2. New purchases by OS version:</strong> In RevenueCat, go to the Customers tab and select “New audience”. Add a filter for “First purchase date” with the last 90 days. Click the “Export all” button and then have your favorite AI tool analyze the data.For Weather Up, 85% of new purchases in the past 90 days were on iOS 26 or newer.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/176b174ad0036c427420e20f66b697072c40100f-1774x810.png" alt=""/></figure>
<p><strong>3. Active subscribers by last seen OS version: </strong>In RevenueCat, go to the Customers tab and select “Active subscribers”. Click the “Export all” button and then have your favorite AI tool analyze the data.For Weather Up, 94% of active subscribers are on iOS 26 or newer. Interestingly, 8.5% of active Weather Up subscribers are already on the iOS 27 beta.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/2a4a84533709c97b1669411c80c6d4f38f6a9b6b-1762x648.png" alt=""/></figure>
<p><strong>4. What the floor buys you.</strong> If requiring iOS 26 doesn’t change what you can ship or how fast, there’s no trade to make. For a Liquid Glass-heavy UI or anything built on iOS 26-only frameworks, the velocity gain is real.</p>
<p>So, did the data change my mind about Weather Up? No. But this is a judgment call: potentially losing 15% of new purchases is a real cost, and more than I expected when I tweeted.</p>
<p>Two things tip the decision anyway. We’re deep into a 4.0 update that fully embraces Liquid Glass, and dragging that redesign through a second, older design system is exactly the tax this whole post is about.</p>
<p>The second: the past 90 days of data is somewhat skewed by traffic source. Most of Weather Up’s current traffic comes from App Store search and being featured, which reach the broadest (and least bleeding-edge) audience. When the 4.0 update launches alongside iOS 27 in the fall, press and launch attention should skew new downloads back toward early adopters, closer to the 94% of active subscribers. Your new-payer mix isn’t a fixed property of your app; it follows your traffic.</p>
<p>So pick your line on purpose, and treat it as a line, not a law. David Smith’s is 1% of new downloads. Mine is “under 10% of new paying users, if the velocity gain is real”, and Weather Up just showed you both halves of that sentence doing work. What’s not defensible is picking a minimum on vibes (or this post) without ever looking at your own data.</p>
<aside class="tip"><strong>Keep reading</strong><p><a href="https://www.revenuecat.com/blog/engineering/wwdc26-whats-new-for-apps/">WWDC26: What’s new for subscription apps</a></p></aside>
<p>
</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[You could give a founder $20 million to hire engineers. Without product taste, they’d light it all on fire.]]></title>
      <link>https://www.revenuecat.com/blog/growth/andrew-maguire-volo-ventures-sub-club-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/andrew-maguire-volo-ventures-sub-club-podcast-2026</guid>
      <pubDate>Wed, 08 Jul 2026 13:11:40 GMT</pubDate>
      <dc:creator><![CDATA[David Barnard]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Andrew Maguire helped scale two App of the Year winners and now invests in consumer apps at Volo Ventures — and he's telling founders the best path to $10 million is to never raise a dollar of venture capital.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/94860accc61941efb56dd1df2e4f7c50e42087af-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<h2>The $20 million bonfire</h2>
<p>Five years ago, the path to building a consumer app was straightforward: raise venture capital, hire the best engineers, and build. Andrew Maguire thinks that playbook was always flawed — and now it’s irrelevant.</p>
<p>“You could give a founder $20 million to hire engineers and they can go hire all the best engineers,” he says. “And if they don’t have great product intuition and product taste and a process for building a great product, then they’re just going to light $20 million on fire.”</p>
<p><a href="https://www.youtube.com/watch?v=1R1_ZbmLyJI">Watch on YouTube</a></p>
<p>AI has collapsed the cost of building to near zero. RevenueCat co-founder Jacob Eiting agrees that the bottleneck has shifted: “The noise has gone up so much and there’s just so many options now that almost the only way to differentiate is by having something that stands out.” Product quality drives down acquisition costs, which drives efficient scale. Engineering talent is no longer the scarce resource. Taste is.</p>
<p>Maguire is particularly skeptical of the “one-shot app” approach — prompting an AI to generate a complete product from a few sentences. “Build me a great personalized fitness app and here’s a couple other sentences — because there’s just too many decisions to make.” The founders who’ll win are the ones using AI to iterate faster through those decisions, not to skip them.</p>
<h2>The clearest lane to $10 million has no VCs in it</h2>
<p>The conversation’s most striking claim came from Maguire mid-episode: “There has never been a better time to indie developer a $10 million app.”</p>
<p>The logic is simple. The old dynamic required venture capital to build anything meaningful — you needed engineers, infrastructure, and runway. VCs would then ask how you planned to disrupt Chess.com’s network effects, and if you couldn’t answer that, you couldn’t raise, and if you couldn’t raise, you couldn’t build. That entire chain has broken.</p>
<p>“Now there’s a very clear lane to build those products super cash efficiently,” Maguire says. Subscription infrastructure, revenue-based UA financing, and AI-assisted development mean a solo developer can reach eight figures without giving up equity.</p>
<p>Eiting reinforced this with a real example: Curtis Herbert of Slopes, who famously bootstrapped his ski tracking app to a team of roughly 12 people. “He’s built a really amazing business and he’s very happy with how it is. He did a very thoughtful and conscious way of choosing how to build that business.”</p>
<p>The practical implication: if you can’t clearly articulate how your company becomes worth a billion dollars, don’t raise venture. “Those investors get preferred stock,” Maguire explains. “If you sell your company for $20 million, they get all $20 million.” The math only works if you’re genuinely building toward a multi-billion dollar exit.</p>
<h2>What actually makes consumer investable in 2026</h2>
<p>For the apps that do warrant venture capital, Maguire has a simple filter: “You need to have a credible path to building a low churn product.”</p>
<p>That means one of two things. Network effects — hard to create, nearly impossible to disrupt once established. Or deep AI-powered personalization that creates usage lock-in, where the product becomes genuinely better the longer someone uses it.</p>
<p>“Think about a product that is going to be much better for the user in two years or three years than it is when they first started using it,” he says. “Not because of the promise of more features being shipped, but because of the usages.” Health, finances, nutrition — any domain where a full-time personal coach with access to 100% of your data would be immensely valuable.</p>
<p>The catch: these inference-heavy products are expensive to run. Maguire acknowledges the tension. “Once you taste the good model, I can’t go back. I want to use it for everything, which is really expensive.” His thesis is that this is precisely where venture capital makes sense — fund the gap between viral growth and the inevitable decline in inference costs. “Let the VCs bet on the fact that the cost of inference will come down.”</p>
<h2>Why apps aren’t going anywhere</h2>
<p>Near the end of the conversation, Maguire dismissed two “existential threats” that keep surfacing in industry discourse: that agents will replace apps, and that everyone will just build their own bespoke software.</p>
<p>“One of the classic mistakes in a venture-backed business is you get some company, they’re crushing it, they’re super confident, they’re growing really fast, and then the CEO’s like, ‘I don’t like Salesforce. We’re going to build our own CRM and it’s going to be better.’ And then it becomes a total boondoggle.”</p>
<p>The same logic applies to consumers building their own tools. It’ll happen at the margins — and it’ll compete on price with commercial software — but it won’t eliminate the market for dedicated products built by focused teams.</p>
<p>“People want beautiful visual experiences,” Maguire says. “It’s not only going to be agents talking to each other. People want to interface with something using their eyeballs.” The form factor might evolve, but the demand for taste-driven, purpose-built apps isn’t going away.</p>
<p>In <a href="https://www.youtube.com/watch?v=1R1_ZbmLyJI">the full episode</a>, Andrew, David, and Jacob also discuss the layered fee structures that erode LP returns in venture, why General Catalyst’s cohort-based UA financing model is interesting for mid-stage apps, and Andrew’s experiment building self-improving CRM tools with AI agents.</p>
<h2>Guest links</h2>
<p><a href="https://www.linkedin.com/in/andrewmaguire/">Andrew Maguire on LinkedIn</a></p>
<p><a href="https://x.com/AndyKMaguire">Andrew Maguire on X</a></p>
<p><a href="https://www.voloventures.com/">Volo Ventures</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Recover churned subscribers automatically with win-back campaigns for web]]></title>
      <link>https://www.revenuecat.com/blog/company/win-back-campaigns-for-web</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/win-back-campaigns-for-web</guid>
      <pubDate>Tue, 07 Jul 2026 13:02:58 GMT</pubDate>
      <dc:creator><![CDATA[Niklas Winkels]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[Win back subscribers who cancel, automatically. When someone's access expires, we send them an email with a discount offer. They resubscribe on the web, so you keep more of the money.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/086af4789af13c386461cee35b0677fc2e8c64d4-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Every app loses subscribers. And while both Apple and Google have native win-back offers, they take real work to set up: StoreKit 2 integration, App Store Connect configuration, eligibility rules, image assets, App Review. On Android, win-back offers can only be redeemed inside your app, so the user has to come back on their own.</p>
<p>What if you could reach churned subscribers via email, the moment their access expires? And what if the re-subscription happened on the web, where you keep more of the revenue?</p>
<p>That’s what win-back campaigns for web does. It’s now in public beta.</p>
<h2>2 settings, 60 seconds, done</h2>
<p>You configure a campaign in the RevenueCat dashboard in about 60 seconds. There are two settings: pick the app you want to monitor for subscription expirations, and choose a Web Purchase Link with your win-back offer. That’s it.</p>
<p>When a subscriber’s access expires (not when they cancel, but when they actually lose access), RevenueCat sends them a transactional email with a “Claim offer” button. That button takes them to your web checkout, where they re-subscribe at whatever discounted price you’ve configured. For example, a first year at $39.99 instead of $99.99 that auto-renews at the standard price.</p>
<p>The trigger fires only on genuine end-user churn. If you revoke access or cancel a subscription from your side, no email gets sent.</p>
<p>The email is branded to your app. It pulls in your app icon, name, and appearance settings automatically. The template includes a clear “Claim offer” call to action linking to your Web Purchase Link. You don’t need to design, write, or host anything. In the current beta, the template uses a standard layout and English copy. Email customization is on the roadmap.</p>
<h2>Reach users that native offers can’t</h2>
<p>Native win-back offers from Apple and Google are powerful, but they rely on the user returning to the App Store, opening your app, or checking their subscription settings. They’re passive. Your offer sits there and waits.</p>
<p>Win-back campaigns reach out at the exact moment access expires, via email, regardless of whether the user still has your app installed or what platform they originally subscribed on. A churned iOS subscriber, an Android subscriber, or a web subscriber all get the same email.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/9ce527b08a902510e2fc0222edee0c5ac64a209d-2272x1368.png" alt=""/></figure>
<p>And because the re-subscription happens on the web, you bypass the 15%–30% platform commission entirely. You can offer a generous discount and still improve your effective revenue per recovered subscriber. It’s money from users who would have otherwise left completely.</p>
<p>If you already have Apple or Google win-back offers configured, this works alongside them. More places for a churned user to resubscribe is a good thing.</p>
<h2>Send them anywhere: paywall or checkout</h2>
<p>The “Claim offer” link points to the Web Purchase Link you’ve configured. Depending on how you’ve set that up, the user can land on a paywall, a package selection screen, or go directly to checkout. You control the experience.</p>
<p>You can pair this with <a href="https://www.revenuecat.com/docs/web/web-billing/discounts">Flexible Discounts</a> to create the right offer: introductory pricing on a yearly plan, percentage-off codes, or whatever makes sense for your audience.</p>
<h2>Their account picks up where it left off</h2>
<p>The email link includes the user’s existing app ID. When they complete checkout on the web, their premium access is restored inside your mobile app immediately. The purchase shows up in their existing customer history, attributed to the same user, with the same entitlements.</p>
<h2>One campaign covers iOS, Android, and web</h2>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/03ffa40312c960165e59c10c81f46414e48408d6-592x232.png" alt=""/></figure>
<p>Win-back campaigns work with all three of RevenueCat’s supported web billing engines: RevenueCat Billing, Stripe Billing, and Paddle Billing.</p>
<p>The app you monitor for expirations can be iOS, Android, or web. This means the feature supports both <strong>app-to-web</strong> (a mobile subscriber churns and re-subscribes on web) and <strong>web-to-web</strong> (a web subscriber churns and re-subscribes on web) flows.</p>
<h2>What you need before creating a campaign</h2>
<p>Win-back campaigns build on top of RevenueCat Web. Before you create one, you’ll need:</p>
<ul>
<li><strong>A web configuration.</strong> Connect a billing engine (RevenueCat Billing, Stripe Billing, or Paddle Billing) by following <a href="https://www.revenuecat.com/docs/web/overview">Getting started with RevenueCat Web</a>.</li>
<li><strong>A Web Purchase Link with your win-back offer.</strong> Create an offering with a discounted product (like a yearly plan with an introductory price), then create a Web Purchase Link for it. This is what the email links to.</li>
<li><strong>Your customers’ email addresses.</strong> RevenueCat sends the win-back email to the address stored in the <code>$email</code> <a href="https://www.revenuecat.com/docs/subscriber-attributes">subscriber attribute</a>. If your app already collects email during signup or onboarding, you’re set. If not, you’ll need to start setting it via the SDK.</li>
</ul>
<h2>Getting started</h2>
<p>Once those are in place, creating a campaign takes seconds:</p>
<ol>
<li>Go to <strong>Lifecycle → Win-back</strong> in your RevenueCat dashboard</li>
<li>Click <strong>Create campaign</strong></li>
<li>Select the app to monitor for subscription expirations</li>
<li>Select the Web Purchase Link containing your win-back offer</li>
<li>Click <strong>Create campaign</strong></li>
</ol>
<p>The campaign starts monitoring immediately.</p>
<p><a href="https://www.revenuecat.com/docs/web/winback-campaigns">Set up your first win-back campaign →</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Announcing Shipaton 2026: Ship an app, win big, join the fun]]></title>
      <link>https://www.revenuecat.com/blog/company/announcing-shipaton-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/announcing-shipaton-2026</guid>
      <pubDate>Thu, 02 Jul 2026 09:49:18 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti, Charlie Chapman]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA[Shipaton 2026 — RevenueCat's global hackathon for mobile app builders, August 1 to September 30]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/e417ec04f4999f8c31deeb776436a0d2f12a8e60-2482x1160.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p><a href="https://www.youtube.com/watch?v=3yPFdwcSiok">Watch on YouTube</a></p>
<p>Shipaton is back. This <strong>August and September</strong>, we’re inviting builders from all corners of the globe to ship brand-new apps and compete for a share of <strong>over $1 million worth of prizes</strong>.</p>
<p>There’s a lot to cover, so here’s the TLDR:</p>
<ul>
<li><strong>Ship a new app</strong> in August or September, and you could win part of over $1 million worth of prizes</li>
<li><strong>New categories</strong>, including a brand-new student category</li>
<li><strong>ShipKit is back</strong> with tons of discounts and deals for participants</li>
<li><strong>Twice-a-week livestreams</strong> with expert guests and live Q&amp;A</li>
<li><strong>Shipaton IRL</strong> in-person events are back, all over the world</li>
<li><a href="https://shipaton.com/"><strong>Register right now</strong></a> at shipaton.com</li>
</ul>
<p>Alright, let’s dig into the details.</p>
<h2>The challenge</h2>
<p>Your challenge is simple:</p>
<ul>
<li><strong>Ship a brand-new iOS or Android app</strong> to the App Store, Google Play Store, or — new this year — the Samsung Galaxy Store, between <strong>August 1st and September 30th</strong></li>
<li>Use the <a href="https://www.revenuecat.com/docs/getting-started/installation">RevenueCat SDK</a> to power at least one in-app purchase, or serve ads through RevenueCat Ads</li>
</ul>
<p>That means a real app. In a real store. Released for the first time during the Shipaton window. Your app can be something you just started working on, or something you’ve been working on for a longer time but just haven’t shipped yet.</p>
<p>You can start brainstorming, building, posting, and getting people excited before August 1st. But to be eligible, your app has to be released no earlier than August 1st and no later than September 30th. We recommend shipping your 1.0 as quickly as possible to get through App Review, then pushing updates from there.</p>
<h2>Bigger than ever</h2>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/a54a27adb620761ba70ebe09b99eac4c38720bd7-1415x630.png" alt=""/></figure>
<p><a href="https://www.revenuecat.com/blog/company/shipaton-2025/">Last year</a>, you completely blew us away. Close to a thousand new apps launched. Thousands of #BuildInPublic in TikTok and other social media. Communities all over the world hosted Shipaton IRL events. And winning teams ended up in New York, accepting their Shippy trophies on stage, with their apps lighting up a giant billboard in Times Square.</p>
<p>Naturally, we took that to mean that Shipaton 2026 needs to be even bigger. Here’s what’s on the line:</p>
<ul>
<li><strong>Over $700,000 in cash</strong> to date, and that number may grow as more sponsors join the fun</li>
<li><strong>Winning apps featured on giant billboards in Times Square</strong></li>
<li><strong>Flights to New York City</strong> for the Shippies red carpet award ceremony (that’s right, our own award ceremony for people who ship great apps) and RevenueCat’s exclusive <a href="https://appgrowthannual.com/">App Growth Annual</a> conference</li>
<li><strong>Press coverage</strong> for winners on 9to5Mac and 9to5Google</li>
<li><strong>Investor exposure</strong> through the brand-new Shipaton Growth Fund</li>
</ul>
<p>And there are even more prizes we’re still finalizing — we’ll announce those soon.</p>
<h2>New award categories</h2>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/e5ca94f9e13dc9ce5f1d0b0c9685050726ae31ef-1401x600.png" alt=""/></figure>
<p>This year’s lineup of award categories celebrates all the different ways to build a great app. Many you’ll recognize from previous Shipatons, but there are some big new additions.</p>
<h3>Grand Prize: Build &amp; Grow Award</h3>
<p>The big one. This award honors the app that gains the most user traction and growth momentum during the event. We want to hear what you’ve done post-release to push your app’s growth to the next level.</p>
<h3>[NEW] Next Gen Award (students, this one’s for you)</h3>
<p>A brand-new category exclusively for students, designed to remove one of the biggest blockers for new builders: <strong>you don’t need a paid Apple or Google developer account to participate</strong>.</p>
<p>If you’re an active student, you can submit a video of your app plus your open-source code, and we’ll use that to validate your submission. High school, college, university, bootcamp, campus dev club, or another academic program: Shipaton is for you too. Learn more on the <a href="https://www.shipaton.com/next-gen">Next Gen page</a>.</p>
<p>And if you’re a student organizer, teacher, professor, or part of a developer club, this is the perfect excuse to bring Shipaton to your campus. More on that below.</p>
<h3>[NEW] Catvertising Award</h3>
<p>Celebrates the most creative and effective use of RevenueCat Ads as a monetization method. We’re looking for clever placements, smart integration with the rest of your revenue stack, and an experience users don’t hate.</p>
<h3>[NEW] Best Game Award</h3>
<p>Shipaton 2026 welcomes a whole new category of developers: This award goes to the best mobile game shipped during Shipaton. We’re looking for great gameplay, art direction, and a monetization fit that suits the genre.</p>
<h3>#BuildInPublic Award</h3>
<p>For developers who share the most interesting parts of their development journey on social media. We’re looking for compelling lessons learned or ideas incorporated from community feedback.</p>
<h3>HAMM Award (Help Apps Make Money)</h3>
<p>RevenueCat exists to help apps make more money. This award goes to the project with the most robust and creative monetization strategy.</p>
<h3>RevenueCat Design Award</h3>
<p>For the app that best represents the craft of app development, separate from its viability as a business. We’re looking for innovative ideas and/or beautiful design and animations.</p>
<h3>RevenueCat Peace Prize</h3>
<p>Awarded to the project that provides the greatest social good. We’re looking for apps with big benefits to communities or society at large.</p>
<p>On top of all that, our category sponsors will have dedicated award categories too. More details coming soon. Check the <a href="https://revenuecat-shipaton-2026.devpost.com/">DevPost page</a> for full judging criteria.</p>
<h2>Supported by the industry</h2>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1f621ead285bda775dba3928d9c2c7693aefde83-1406x513.png" alt=""/></figure>
<p>This year, our sponsors have shown up big. Huge thanks to <strong>Replit, OneSignal, JetBrains, Layers, Noise, Stripe, and Samsung</strong> for coming in as category sponsors for Shipaton 2026.</p>
<p>We’ll share more about their categories soon, but for now, just know they’re helping us make this year’s Shipaton bigger, weirder, and more rewarding for builders. And more sponsors are joining all the time — which means bigger cash prizes and more additions to the ShipKit.</p>
<h2>ShipKit is back</h2>
<p>ShipKit is your bundle of tools, credits, deals, and resources to help you build faster, launch better, and hopefully save a lot of money along the way.</p>
<p>Some of those deals are while supplies last, so the sooner you <a href="https://shipaton.com/">register on DevPost</a>, the sooner you get access as ShipKit starts to roll out.</p>
<h3>New this year: the #ShipatonSale</h3>
<p>If you have an app, tool, service, course, template, or anything else that could help Shipaton builders, you can run a sale for participants. Share it using the <strong>#ShipatonSale</strong> hashtag, and submit it to the Shipaton website, where we’ll list sales being run by tools and services in the community.</p>
<p>So even if you’re not competing, there are still ways to support the builders who are, and get your apps in front of thousands of people.</p>
<h2>Built on community</h2>
<p>Shipaton’s success has always come back to the community. It’s more fun when everyone can see each other building, learning, getting stuck, getting unstuck, and cheering each other on. Even with lots of prizes on the line, you’ve always supported and encouraged each other along the way, and we do our best to support that:</p>
<ul>
<li><strong>The #BuildInPublic award</strong> rewards sharing your building journey online — an amazing way to motivate yourself and inspire others</li>
<li><strong>The </strong><a href="https://discord.gg/shipaton"><strong>Shipaton Discord</strong></a> is open again for discussion, questions, feedback, finding teammates, and getting help from the RevenueCat team</li>
<li><strong>The #post-engagement-boost channel</strong> lets you share your #BuildInPublic posts and boost each other’s work so more people see what you’re building</li>
</ul>
<h3>Twice-a-week livestreams</h3>
<p>Throughout the event, we’ll host livestreams twice a week where you can learn new skills for building, launching, and growing mobile apps. Expert guests, practical sessions, and live Q&amp;A. So you’re not building alone for two months. You’ll have a steady stream of lessons and live support to keep you moving.</p>
<h2>Shipaton IRL: events near you</h2>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/35dd495b7c4158253e42a6edeb27066364f992ed-1360x520.png" alt=""/></figure>
<p>The Shipaton community isn’t only online. Last year, builders and community organizers hosted Shipaton IRL events — meetups, co-working sessions, and launch parties — in cities all over the world. This year, we want even more of that.</p>
<p>Check out <a href="https://shipaton.com/events">shipaton.com/events</a> to find a Shipaton IRL event near you, or apply to host one yourself. Approved hosts get a Meetup-in-a-box with Shipaton swag, food vouchers, and organizer resources, and we’ll promote your event to the massive Shipaton audience.</p>
<p>These are perfect for new or existing developer communities, hacker clubs, and student organizers planning campus events.</p>
<h2>Ready, set, ship!</h2>
<p>Here’s what to do right now: go to <a href="https://shipaton.com/">shipaton.com</a>, click through to DevPost, and register. That’s where we’ll share the latest news, full rules, category updates, sponsor announcements, and ShipKit access as soon as it’s ready.</p>
<p>Shipaton is the perfect motivator to finally build that app idea you’ve had in your brain forever or to ship something totally new, now that app development (with a little help from AI tools) is more accessible than it’s ever been.</p>
<p>We can’t wait to see what you build. Let’s get shipping.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[A patron plan with no extra features outsold expectations — and proved design alone can be a business]]></title>
      <link>https://www.revenuecat.com/blog/growth/andy-allen-not-boring-software-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/andy-allen-not-boring-software-launched-podcast-2026</guid>
      <pubDate>Wed, 01 Jul 2026 12:48:15 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Andy Allen built a 2-person app studio whose most expensive subscription tier unlocks essentially no extra features — and it outsells expectations every year.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/35f715d0f57b98df12e6fbbd91d6cef68a9c0dae-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<h2>The artist who burned his paintings at 40</h2>
<p>Not Boring Software didn’t start with a market opportunity or a gap analysis. It started with the conceptual artist John Baldessari.</p>
<p>Baldessari spent the first half of his life as a self-described “mediocre landscape painter.” Just before turning 40, he took his entire body of work — decades of paintings — and burned it all. Then he wrote “I will not make any more boring art” thousands of times on camera, in a kind of performance art ritual. He went on to become one of the most influential artists of his generation.</p>
<p><a href="https://www.youtube.com/watch?v=TCrwOUDifVM">Watch on YouTube</a></p>
<p>Andy Allen found Baldessari at almost exactly the same point in his own career. He was approaching 40, had spent the prior few years living in Amplitude dashboards at WeTransfer (which had acquired his previous company, FiftyThree), and felt himself slipping into the kind of B2B product work that was fashionable but soul-deadening.</p>
<p>“I felt like sometimes there’s just like good very simple heuristics,” Allen says. “If you can just say, ‘I will not make any more boring software,’ that might lead to some good outcomes.”</p>
<p>The timing was deliberately uncool. In 2020, nobody was starting an app business. Everyone was chasing SaaS. “It’s so funny because I just found that oftentimes the things that I’ve done that have had the most staying power or resonated the most have often been these very uncool things,” Allen says. “Whenever you try to do something cool or of the moment, it just doesn’t really go anywhere or it flubs.”</p>
<h2>The patron plan that shouldn’t have worked</h2>
<p>Not Boring launched with three apps — Weather, Calculator, and Timer — available individually or as a collection via subscription. For Weather, the subscription pitch was straightforward: premium data sources. For Calculator and Timer, what you were unlocking was… skins. Different visual themes. Alternative app icons.</p>
<p>Conventional wisdom says this shouldn’t work. People don’t pay for aesthetics in utility software. But Allen and his co-founder added something else: a “patron plan” priced at five times the standard tier. It included essentially nothing extra — a physical gift, and the knowledge that you were funding the studio’s work.</p>
<p>“It was shocking how many people went for that plan,” Allen says. “And that was kind of the moment that we realized, oh yeah, this is a little bit of a different business model. We’re not selling features here. People want to see this work and see us maybe as the knights fighting this battle against boring software and they want to fund this effort.”</p>
<p>The model has compounded over five years. Early subscribers got their price locked in permanently. Every new app (one per year) is automatically included. The incentive structure pushes Allen toward making new, interesting things rather than grinding on feature parity with competitors. “We’re not trying to chase features that are going to unlock some new audience for us or that we think are going to suddenly bump up our conversion rates,” he says.</p>
<h2>Why every Not Boring app ships as a “complete” product</h2>
<p>Allen’s previous company, FiftyThree, made Paper — the drawing app that became one of the first native-feeling iPad apps and eventually sold to WeTransfer. That experience taught him what happens when you raise VC money for a product that’s a great small business but not a billion-dollar outcome: you get trapped.</p>
<p>“The biggest fear was getting trapped in a business that you didn’t want to run,” Allen says. “And I see that happen with so many founders.”</p>
<p>The structural solution at Not Boring is radical simplicity. Each app ships as a complete product. No V3. No feature bloat. No chatbot in the corner. “I prefer to say like, ‘This is the calculator that we made.’ It’s complete and it does its job really well. We’re not going to change it on you.”</p>
<p>This creates a forcing function: if you can’t go back and iterate on old apps, you have to make new ones. And making new things — re-envisioning old software categories through a design-first lens — is the work Allen actually wants to do. The constraint is the point.</p>
<h2>A dozen sound files for one button</h2>
<p>The Not Boring apps feel like video games. Everything runs in Apple’s 3D engine (SceneKit), with low-poly models inspired more by the PS1 era than photorealism. But the detail that most people notice first is the sound.</p>
<p>Allen plays sounds even when the iPhone’s mute switch is on — something that would get most apps destroyed in reviews. It works here because the sound is the experience. But the real trick is subtler: every button tap doesn’t play one sound file. It plays one of a dozen slightly different versions of the same click.</p>
<p>“No button ever sounds exactly the same,” Allen explains. “Why does it sound so great to listen to someone typing on an old clickety-clackety keyboard? It’s because of all the variation. When you hit a physical button, it hits differently every time.”</p>
<p>This is a technique borrowed directly from game audio, where footsteps and punches need to repeat without becoming grating. Allen published the technique — along with free sound kits — hoping other developers would adopt it. “It doesn’t take much, honestly. It just takes working with a sound designer,” he says. “It’s a great way to amplify your experience that really doesn’t add much overhead or complexity to the app itself.”</p>
<h2>3 years to build a camera that launches faster than the competition</h2>
<p>Not Boring Camera took three years of false starts before shipping. The team explored 3D photography, spatial photos, and various experimental directions before arriving at a simpler insight: don’t reinvent what a camera does — reinvent what it feels like to use one.</p>
<p>Every control in the camera app is modeled in 3D. Dials turn. Sliders have physical weight. Buttons tilt based on where you press them. The preview isn’t full-screen — it’s inset, like looking through a viewfinder. And despite running in a game engine, the app launches faster than every third-party camera app Allen has tested.</p>
<p>But the real philosophical bet is on the image pipeline. Not Boring Camera strips out all of Apple’s computational photography — the multi-frame composites, noise reduction, and HDR tone mapping — and works directly with raw sensor data. Allen calls it “Super Raw” (a nod to Apple’s “ProRAW,” which he argues is still heavily processed).</p>
<p>“The default camera apps — it’s not just Apple, it’s Google, it’s everyone — their goal is to make sure that you never take a bad photo,” Allen says. “But I think the result is that you never can really take a great one.”</p>
<p>The app applies film-inspired LUTs in real time, lets you edit raw exposure and temperature before shooting, and embraces grain and blur as creative choices rather than flaws. “Some of my favorite photos from our app are accidents,” Allen says. “It’s like I accidentally hit the shutter button as I was putting it away so it’s like this really cool blur. I miss some of that and I think we’re trying to bring some of that back.”</p>
<p>In <a href="https://www.youtube.com/watch?v=TCrwOUDifVM">the full episode</a>, Andy also talks about designing the never-shipped Microsoft Courier tablet, why the early Macintosh shaped his view of computers as creative tools, and his recommendations for checking out the artist John Baldessari and musician Brian Eno as models for creative thinking.</p>
<h2>Guest links</h2>
<p><a href="https://x.com/asallen">Andy Allen on X/Twitter</a></p>
<p><a href="https://notbor.ing/">Not Boring Software</a></p>
<p><a href="https://apps.apple.com/app/not-boring-camera/id6504163047">Not Boring Camera on the App Store</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Your app is perfectly optimized. That’s why nobody remembers it.]]></title>
      <link>https://www.revenuecat.com/blog/growth/optimized-app-forgettable</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/optimized-app-forgettable</guid>
      <pubDate>Tue, 23 Jun 2026 09:43:08 GMT</pubDate>
      <dc:creator><![CDATA[Olha Yohansen]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[When every app copies the same playbook, the only advantage left is meaning something.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/95b7b046519d94aefad6635dbdfd16fd4753c9a0-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>There is a moment most of us have had. You open a habit tracker, a period tracker, a sleep app, and for a second you are not quite sure if it is the one you downloaded last week or the one you have been using for months. The icon looked familiar. The onboarding felt familiar. The paywall was… definitely familiar.</p>
<p>This is not a coincidence, and it is not anyone’s fault.</p>
<p>For as long as apps have been a business, the advice has been consistent: look at what is already working. Find the apps that are converting, understand why, get inspired by the patterns that have been proven. This is genuinely good advice. It is how most successful products got to where they are. Learning from what works is faster, cheaper, and less risky than starting entirely from scratch. I have followed this advice myself, and I have given it to others.</p>
<p>But something happens when an entire industry follows the same advice all the time. The patterns converge. The quiz-style onboarding. The projection graph that dramatically reveals your results, then changes four times as you progress through the funnel promising you that dream weight goal achieved by your next holiday. The interrupting pop-ups on pre-paywall loaders. The three-tier paywall with the annual plan highlighted and the countdown timer. The CTA copy that has been A/B tested into the least interesting version of itself. These things work, until everyone is doing them, and then they stop being an advantage. They become the baseline.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/52dc06bfcf7b5c45d5e171c70b2e2db9fab6ed8c-1603x1704.png" alt=""/><figcaption>Same heading, same diagram, same labels, same body copy. Flo Health (left) and Clover (right).</figcaption></figure>
<p>This is what “get inspired by competitors” looks like when the ‘iterate’ step gets skipped. Getting inspired is fine. Leaving it there is not. When users compare apps, they notice. Please have some <s>ethics</s> heart.</p>
<p>And this pattern goes deeper than a single screen. Look at what happened with the personalized weight-loss graph. Noom built it: a projected weight loss curve tied to a personal date, a specific goal, even a life occasion. “And reach your goal by the vacation.” It was a genuinely good idea that worked.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/bac5e9b7e54e128f3e0eb4395d1690db27f7e7f4-1601x820.png" alt=""/><figcaption>Noom’s personalised weight-loss projection (far left) spawned near-identical versions across the category. The rightmost example added milestone markers and social proof – same starting point, genuinely different idea.</figcaption></figure>
<p>The copies came quickly. Almost everyone who tested it saw an uplift. The headline – “the last plan you’ll ever need” – spread across the category until almost every app in weight loss was apparently offering the definitive final solution to the human condition millions have still not solved. Most of the copies stopped there.</p>
<p>But one app went further. It kept the projection concept, then added intermediate weight milestones along the curve so the user could see the journey in stages, not just the destination. They did it because they knew their product well, they knew the result looked like ‘steps’ with short ‘walls’ – their users told them that in these same words. Then they placed social proof from those real users directly below the graph, at the exact moment the user might feel sceptical about whether the prediction is real. That is iteration. Same starting point, genuinely different idea, serving the user better at a specific moment of doubt. This is what I call ‘heart’.</p>
<p>I have worked with a number of B2C apps across health, wellness, and productivity. I have seen a lot of funnels from the inside. And what I notice more and more is that the challenge has quietly shifted. Building a well-structured app, with a clean onboarding flow and a tested paywall, is no longer what separates the apps that grow from the ones that do not. There are companies out there who have onboarding funnel constructors with proven screens, sections, even CTAs. The framework is defined – nothing to invent there.</p>
<h2><strong>The real question now</strong></h2>
<p>The harder question now is: once you have paid to get someone’s attention, what makes them remember you, choose you and stay with you?</p>
<p>Building has never been easier. Getting memorable has never been harder. That gap is where the real work happens now.</p>
<p>I want to be upfront about something before we go further. This is not a case against learning from competitors. I have spent years advising apps to do exactly that, across more than twenty products I have worked with as a consultant in the past few years. Look at what is working, understand why, adapt it for your context, move fast. I still give that advice. It is the right starting point.</p>
<p>But there is a step that routinely gets skipped. The “iterate quickly” part, the part where you take what you have learned and make it genuinely yours, tends to get dropped in favour of just shipping. And I understand why. You need momentum. You need to see if the model works before you invest in making it feel like something. That is reasonable.</p>
<p>The problem is that “making it yours” never comes after, because there is always another thing to ship. And so the app stays at the “inspired by competitors” stage indefinitely.</p>
<p>What I am arguing here is that making it yours is not a polishing step – it is the thing that determines whether users stay. And it has two sides, read on.</p>
<p><strong>The first is internal</strong>: it has to come from what you actually believe about the problem you are solving. Why did you build this product? What did you see that others were missing? What do you genuinely think users need? That conviction, when it comes through in the product, is what makes an app feel like it was made with care rather than assembled from a template. It’s usually the gut feeling you already have but might be too worried about testing – you have my permission to go ahead and test it out – those things usually work in a surprising way.</p>
<p><strong>The second is external</strong>: it has to speak to the user in their own words. Not your words about them. Their words about themselves. User research, interviews, even reading App Store reviews in your category give you the vocabulary your users actually use to describe their own problems. When that vocabulary shows up in your onboarding, in your push notifications, in your emails, users feel understood in a way they cannot quite name but definitely feel.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/628a015571c89ab2613071b90ed65ca68c9f9b96-1601x1153.png" alt=""/><figcaption>Three apps that speak the user’s language from the first screen: Flo confirms you’re in the right place with social proof, How We Feel asks goals in the user’s own framing, and Fabulous turns onboarding into a personal commitment.</figcaption></figure>
<p>The practical starting point is user research: interviews, App Store reviews in your category, even the language people use on Reddit threads about their problem, tweets [see above]. That vocabulary, the words real users use to describe their own situation, is what your onboarding questions should be built from. When it comes from genuine understanding of the user rather than internal product language, users feel it. They cannot always name why an onboarding feels right. but they vote with their wallet.</p>
<p>That combination, believing in what you are building and speaking the user’s language, is what I mean by heart throughout this article. It is specific, and it is learnable.</p>
<h2>The shift we’re entering: from careful to alive</h2>
<p>Building an app used to be slow and expensive. You validated carefully, prioritized ruthlessly, shipped incrementally. RICE scores ruled the roadmap. Anything that was hard to measure (delight, tone of voice, a moment of surprise) got cut. Did not score well enough. None of the competitors have that screen. Next.</p>
<p>AI-assisted development, simplified releases you control on the back-end and tools that have recently emerged have dramatically reduced the cost of building and iterating. No-code tools are genuinely capable. A solo founder or an IC Growth operator can ship new concepts in days. This is exciting.</p>
<p>Here is the catch: distribution is harder than it has ever been.</p>
<p>The number of new subscription apps launching monthly has exploded: from roughly 2,000 per month in early 2022 to over 14,700 by early 2026, according to RevenueCat’s <a href="https://www.revenuecat.com/state-of-subscription-apps/">2026 State of Subscription Apps report</a>. App Store search is more competitive. User acquisition costs always keep climbing. Organic growth is rare and unreliable.</p>
<p>And many of these apps, built from the same templates and UX patterns AI picked up from apps that copied each other, end up feeling identical. AI picked the design. AI picks it for everyone. [funny story – when writing this article I gave Claude the two discharge screenshots from earlier and it immediately identified ‘the original’ and cited a pattern of what got copied and why].</p>
<h2>The missing piece: heart alone doesn’t scale</h2>
<p>There is a tempting counter-move, and I have seen it play out. The response to “everything feels the same” is to swing the other direction entirely: build from the heart. Make it personal. Build the app you would want to use. Obsess over the details.</p>
<p>This is not wrong. I am a firm believer in it, and genuinely happy we are living in a moment where building from a real place is possible again, without the six-month approval process killing the idea before it ships.</p>
<p>But heart alone does not get you anywhere if no one finds you. And if the people who do find you cannot figure out how to subscribe.</p>
<p>The apps that win in this environment need three things working at the same time:</p>
<ul>
<li><strong>Something that resonates</strong>: a specific user, a real problem, messaging that lands</li>
<li><strong>Distribution</strong>: a well-tuned paid acquisition engine, a product loop that earns organic growth, or both</li>
<li><strong>A system to convert and monetize</strong>: an onboarding flow, a paywall, a pricing strategy that translates attention into revenue</li>
</ul>
<p>Pick one and you are halfway there. Pick all three and you are dangerous.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Understanding Google Play’s Billing Choice: a complete guide to offering alternative billing]]></title>
      <link>https://www.revenuecat.com/blog/engineering/play-billing-choice</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/play-billing-choice</guid>
      <pubDate>Tue, 23 Jun 2026 02:28:37 GMT</pubDate>
      <dc:creator><![CDATA[Jaewoong Eum]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Billing Choice comes down to two decisions — who renders the choice screen and where payment happens — over one shared setup and one mandatory sale report.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/99b1fdf103a5621562ad14061d2650fdc368894a-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>For most of Google Play’s history, charging for digital goods inside an Android app meant one path. You integrated Google Play Billing, Google processed the payment, and Google collected its service fee. <a href="https://developer.android.com/google/play/billing/billingchoice/integration">Billing Choice</a> changes the shape of that flow. It lets your app present a screen where the user picks between Google Play Billing and your own billing system, or a link out to your website, and then completes the purchase through whichever option they chose. The idea is simple to state, but the integration carries real weight: you decide who draws the choice screen and where the money is collected, and you take on an obligation to report every alternative sale back to Google.</p>
<p>In this article, you’ll explore what Billing Choice changes about the purchase flow, the four integration scenarios that fall out of two independent decisions, the <code>BillingProgram</code> API surface for enabling the program and checking its availability, how each scenario launches its purchase, the external transaction token that ties every alternative sale to a server side report, how subscription upgrades and downgrades behave, and the UX rules that keep the choice screen fair.</p>
<h2><strong>What Billing Choice actually changes</strong></h2>
<p>Google describes the program plainly: the billing choice program lets you integrate your own billing system or guide users to your website for purchases using external web links. Whichever option you implement, the user must still be offered a choice between Google Play Billing and your alternative.</p>
<p>That last clause is the heart of it. Billing Choice is not “alternative billing instead of Google Play.” It is “alternative billing alongside Google Play, with the user deciding.” Google Play Billing never disappears from the screen. Your job is to present a fair choice and then honor whichever side the user taps.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1d86b5831466bd6c7c27f540b768e9403cf5b05b-1265x1999.png" alt=""/></figure>
<p>The program is exposed through a <code>BillingProgram</code> enum, and the value you work with is <code>BillingProgram.BILLING_CHOICE</code>. You pass that value into nearly every Billing Choice call, which signals the shape of the API: alternative billing is configured as a named program you opt into, not a single global switch.</p>
<p>Two independent decisions define how your integration looks:</p>
<ul>
<li><strong>Who renders the choice screen</strong>: Google can draw it for you (<code>ChoiceScreenType.GOOGLE_RENDERED</code>), or you can draw your own (<code>ChoiceScreenType.DEVELOPER_RENDERED</code>) as long as you follow the UX guidelines.</li>
<li><strong>Where the payment happens</strong>: the user pays inside your app through your own billing system (<code>DeveloperBillingType.IN_APP</code>), or you send them to your website through an external web link (<code>DeveloperBillingType.EXTERNAL_LINK</code>).</li>
</ul>
<p>The <code>DeveloperBillingType</code> matters beyond the user experience. It is recorded inside the transaction token you generate, so Google knows whether a given alternative sale was an in app transaction or an external link transaction when it assesses the service fee.</p>
<h2><strong>The four integration scenarios</strong></h2>
<p>Those two decisions combine into the four scenarios that organize the entire integration. Everything else in this guide is a variation on one of these four rows.</p>
<table>
<thead><tr>
<th><p>Scenario</p></th>
<th><p>Choice screen</p></th>
<th><p>Payment location</p></th>
<th><p>How the purchase launches</p></th>
</tr></thead>
<tbody>
<tr>
<td><p><strong>1A</strong></p></td>
<td><p>Google renders it (<code>GOOGLE_RENDERED</code>)</p></td>
<td><p>In app (<code>IN_APP</code>)</p></td>
<td><p><code>launchBillingFlow()</code> with a developer billing option enabled</p></td>
</tr>
<tr>
<td><p><strong>1B</strong></p></td>
<td><p>You render it (<code>DEVELOPER_RENDERED</code>)</p></td>
<td><p>In app (<code>IN_APP</code>)</p></td>
<td><p>You handle the tap yourself, then process payment and report</p></td>
</tr>
<tr>
<td><p><strong>2A</strong></p></td>
<td><p>Google renders it (<code>GOOGLE_RENDERED</code>)</p></td>
<td><p>External link (<code>EXTERNAL_LINK</code>)</p></td>
<td><p><code>launchBillingFlow()</code> with a link URI and token attached</p></td>
</tr>
<tr>
<td><p><strong>2B</strong></p></td>
<td><p>You render it (<code>DEVELOPER_RENDERED</code>)</p></td>
<td><p>External link (<code>EXTERNAL_LINK</code>)</p></td>
<td><p><code>launchExternalLink()</code> with a link URI and token attached</p></td>
</tr>
</tbody>
</table>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/fbc90b11a288eb6e26696728633d79de2d88e825-2048x1085.png" alt=""/></figure>
<p>Notice the pattern. The “1” versus “2” axis is about where money is collected. The “A” versus “B” axis is about who renders the screen. When Google renders the choice screen (the A scenarios), Google needs a way to tell you the user picked your option, so you register a listener up front. When you render the choice screen (the B scenarios), you already know what the user tapped, so instead of a listener you fetch the assets you need to draw a fair screen and generate the reporting token yourself.</p>
<h2><strong>Prerequisites: library version, enrollment, and Play Console</strong></h2>
<p>Before any code compiles against this API, three things need to be in place.</p>
<p>First, the library. Billing Choice requires Play Billing Library version 9.1 or higher. This is separate from the broader migration deadline you may already be tracking: by August 31, 2026, every new app and every update to an existing app must build against Billing Library 8 or later, with an extension available on request until November 1, 2026. That deadline is about version 8 as a floor for the whole ecosystem. Billing Choice sits well above it at 9.1, so adopting the program puts you well past that floor.</p>
<p>Second, enrollment. You must enroll in the program and review its requirements before calling the APIs. If you intend to offer external web links, you also declare that preference in Play Console before you ship the integration.</p>
<p>Third, Play Console configuration. You set your choice screen preference (whether Google renders it or you do) and your external web links preference in the Console. You can also upload an image asset that represents the payment methods your own billing option accepts, to be shown on the choice screen. That asset follows strict specifications:</p>
<table>
<thead><tr>
<th><p>Asset specification</p></th>
<th><p>Value</p></th>
</tr></thead>
<tbody>
<tr>
<td><p>Overall image size</p></td>
<td><p>192dp x 20dp</p></td>
</tr>
<tr>
<td><p>Single card size</p></td>
<td><p>32dp x 20dp</p></td>
</tr>
<tr>
<td><p>Spacing between cards</p></td>
<td><p>8dp</p></td>
</tr>
<tr>
<td><p>Inner padding</p></td>
<td><p>3dp</p></td>
</tr>
<tr>
<td><p>Card outline</p></td>
<td><p>1dp inner stroke, 2dp radius, color <code>#E0E0E0</code></p></td>
</tr>
<tr>
<td><p>Card background</p></td>
<td><p>Solid color, preferably white</p></td>
</tr>
<tr>
<td><p>File format</p></td>
<td><p>PNG with transparent background</p></td>
</tr>
<tr>
<td><p>Maximum payment methods</p></td>
<td><p>5</p></td>
</tr>
</tbody>
</table>
<h2><strong>Enabling the billing program</strong></h2>
<p>You turn on Billing Choice when you build the <code>BillingClient</code>. The new piece is <code>enableBillingProgram()</code>, which takes an <code>EnableBillingProgramParams</code> configured with <code>BillingProgram.BILLING_CHOICE</code>.</p>
<p>The following setup is for a scenario where Google renders the choice screen:</p>
<pre><code class="language-kotlin">val params = EnableBillingProgramParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .setDeveloperProvidedBillingListener(developerProvidedBillingListener)
    .build()

val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(pendingPurchasesParams)
    .enableBillingProgram(params)
    .build()</code></pre>
<p>The important detail is <code>setDeveloperProvidedBillingListener()</code>. You register this listener only when Google renders the choice screen, because Google needs a callback to reach when the user picks your billing option rather than Play’s. The <code>purchasesUpdatedListener</code> you already know that standard Billing handles the case where the user picks Google Play. The two listeners sit in different places, the familiar <code>PurchasesUpdatedListener</code> on the client and the <code>DeveloperProvidedBillingListener</code> inside <code>EnableBillingProgramParams</code>, but together they cover the two outcomes of the choice screen.</p>
<p>When you render your own choice screen, you omit the <code>DeveloperProvidedBillingListener</code> entirely. You build the same <code>EnableBillingProgramParams</code> with <code>BillingProgram.BILLING_CHOICE</code>, but without <code>setDeveloperProvidedBillingListener()</code>, because you handle the user’s tap yourself and never need Google to call back into your app.</p>
<h2><strong>Checking availability and reading the choice configuration</strong></h2>
<p>Billing Choice is not guaranteed to be available for a given user, device, or region, and the configuration you set in Play Console is delivered to your app at runtime. You check both with <code>isBillingProgramAvailable()</code>. The Kotlin form is a suspend function that returns the result alongside an availability details object:</p>
<pre><code class="language-kotlin">val (billingResult, availabilityDetails) =
    billingClient.isBillingProgramAvailable(BillingProgram.BILLING_CHOICE)

val choiceDetails = availabilityDetails.billingChoiceAvailabilityDetails
if (billingResult.responseCode == BillingResponseCode.OK &amp;&amp; choiceDetails != null) {
    val screenType = choiceDetails.choiceScreenType
    val externalLinkSupported = choiceDetails.isExternalLinkAvailable
    \/\/ Branch your integration on these two values.
}</code></pre>
<p>The <code>BillingChoiceAvailabilityDetails</code> object answers the two questions that decide which scenario you are in. The <code>choiceScreenType</code> property is either <code>ChoiceScreenType.GOOGLE_RENDERED</code> or <code>ChoiceScreenType.DEVELOPER_RENDERED</code>, telling you who is responsible for rendering the screen. The <code>isExternalLinkAvailable</code> property tells you whether external web links are enabled for this user. A robust integration reads both values and falls back to standard Google Play Billing whenever Billing Choice is unavailable, rather than assuming the program is always on.</p>
<p>For callback-style code, there is also <code>isBillingProgramAvailableAsync()</code>, which delivers the same <code>BillingResult</code> and availability details to a listener instead of suspending.</p>
<h2><strong>Scenario 1A: Google renders the choice, payment happens in app</strong></h2>
<p>This is the lightest scenario to integrate, because Google does most of the work. You enable the program with a <code>DeveloperProvidedBillingListener</code>, confirm availability shows <code>GOOGLE_RENDERED</code>, and then launch the flow with a developer billing option attached:</p>
<pre><code class="language-kotlin">val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .build()

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(productDetailsParamsList)
    .enableDeveloperBillingOption(developerBillingOptionParams)
    .build()

billingClient.launchBillingFlow(activity, billingFlowParams)</code></pre>
<p>The call to <code>enableDeveloperBillingOption()</code> is what tells Google to show the choice screen instead of going straight to Play’s payment sheet. From here the flow splits down two paths depending on what the user taps:</p>
<ul>
<li><strong>The user picks Google Play Billing.</strong> The purchase completes through Google, and your existing <code>PurchasesUpdatedListener</code> receives the result exactly as it would without Billing Choice.</li>
<li><strong>The user picks your billing option.</strong> Google invokes the <code>DeveloperProvidedBillingListener</code> you registered and hands you a <code>DeveloperProvidedBillingDetails</code> object that carries an <code>externalTransactionToken</code>. That token is your receipt to take payment through your own system and later report the sale.</li>
</ul>
<p>The split is the whole point. One code path keeps the familiar Google Play purchase, and the other hands control to you with a token already minted. You did not have to draw a screen or generate the token yourself, because in this scenario Google did both.</p>
<h2><strong>Scenario 1B: You render the choice, payment happens in app</strong></h2>
<p>When you render the choice screen, you trade convenience for control. You no longer register a <code>DeveloperProvidedBillingListener</code>, because there is no Google screen to call you back from. Instead, you gather the pieces you need to draw a screen that treats Google Play fairly, then mint your own token when the user chooses you.</p>
<p>The first piece is the Google Play side of the screen. You ask Billing for the image and loyalty message to display for the Play option with <code>getBillingChoiceInfo()</code>:</p>
<pre><code class="language-kotlin">val params = GetBillingChoiceInfoParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .setPlayBillingChoiceImageLayout(GetBillingChoiceInfoParams.ImageLayout.RECTANGULAR_FOUR_BY_ONE)
    .build()

val (billingResult, info) = billingClient.getBillingChoiceInfo(params)
if (billingResult.responseCode == BillingResponseCode.OK &amp;&amp; info != null) {
    val imageUrl = info.playBillingChoiceImageUrl
    val loyaltyMessage = info.playBillingLoyaltyInfo
    \/\/ Render these into your own choice screen.
}</code></pre>
<p>The <code>playBillingChoiceImageUrl</code> is the payment method artwork for Google Play, and <code>playBillingLoyaltyInfo</code> is any loyalty message Google wants shown beside its option, such as a Play Points rewards message. You must load these every time you draw the screen rather than caching them, a requirement covered in the UX section below. For callback style code there is also <code>getBillingChoiceInfoAsync()</code>, which delivers the same <code>PlayBillingChoiceInfo</code> to a listener, and that is the form the UX guidelines reference.</p>
<p>Once the user taps your option, you generate a reporting token with <code>createBillingProgramReportingDetails()</code>, declaring the billing type as in-app:</p>
<pre><code class="language-kotlin">val params = BillingProgramReportingDetailsParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .setDeveloperBillingType(DeveloperBillingType.IN_APP)
    .build()

val (billingResult, details) =
    billingClient.createBillingProgramReportingDetails(params)
if (billingResult.responseCode != BillingResponseCode.OK) return
val transactionToken = details?.externalTransactionToken</code></pre>
<p>Before you charge the user, you also call <code>showBillingProgramInformationDialog()</code>, which presents the legally required disclosure for transacting outside Google Play Billing. The dialog request carries the <code>BillingProgram</code> and the transaction token you just generated. After the disclosure, you run your own payment flow and keep the <code>externalTransactionToken</code> for the server-side report that follows every alternative sale.</p>
<h2><strong>Scenarios 2A and 2B: Sending users to an external web link</strong></h2>
<p>The external link scenarios replace your in app payment with a redirect to your website. In both of them you generate the token yourself with <code>createBillingProgramReportingDetails()</code>, declaring <code>DeveloperBillingType.EXTERNAL_LINK</code> instead of <code>IN_APP</code>. This is the one place the A versus B shorthand bends: even in Scenario 2A, where Google renders the choice screen, you mint the token before launching rather than receiving it through the listener, because the token has to travel with the redirect. You should only attempt these scenarios when availability reported <code>isExternalLinkAvailable</code> as true.</p>
<p>In <strong>Scenario 2A</strong>, Google renders the choice screen, so you still launch through <code>launchBillingFlow()</code>. The difference is that your developer billing option now carries the destination URI, the token you just generated, and a launch mode:</p>
<pre><code class="language-kotlin">val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .setLinkUri(Uri.parse(&quot;https:\/\/www.example.com\/external\/purchase&quot;))
    .setExternalTransactionToken(transactionToken)
    .setLaunchMode(DeveloperBillingOptionParams.LaunchMode.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP)
    .build()</code></pre>
<p>You attach this to <code>BillingFlowParams</code> with <code>enableDeveloperBillingOption()</code> and call <code>launchBillingFlow()</code> as before. If the user picks Google Play, the normal purchase happens. If they pick your option, Google opens your link and the token travels with the redirect.</p>
<p>In <strong>Scenario 2B</strong>, you render the choice screen yourself, so there is no billing flow to launch. You send the user out with a dedicated call, <code>launchExternalLink()</code>:</p>
<pre><code class="language-kotlin">val params = LaunchExternalLinkParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .setLinkUri(yourLinkUri)
    .setLinkType(LaunchExternalLinkParams.LinkType.LINK_TO_DIGITAL_CONTENT_OFFER)
    .setLaunchMode(LaunchExternalLinkParams.LaunchMode.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP)
    .setExternalTransactionToken(transactionToken)
    .build()

billingClient.launchExternalLink(activity, params) { billingResult -&gt;
    if (billingResult.responseCode == BillingResponseCode.OK) {
        \/\/ The user is on the way to your site. Report the sale when it completes.
    }
}</code></pre>
<p>The <code>LinkType.LINK_TO_DIGITAL_CONTENT_OFFER</code> describes what waits on the other side of the link, and the launch mode opens it in an external browser or app. The result callback only confirms that the link launched successfully. It does not tell you the purchase succeeded, because that happens on your website, outside the app. Completing and recording the sale is your responsibility from this point on.</p>
<h2><strong>The external transaction token: reporting every alternative sale</strong></h2>
<p>Across all four scenarios, the same object keeps appearing: the <code>externalTransactionToken</code>. It is the thread that connects a tap on the choice screen to a line item in your Play revenue report, and understanding it is what separates a working integration from a compliant one.</p>
<p>Every alternative billing transaction, whether it happened in app or through an external link, must be reported back to Google Play. The token is how Google links your report to the original choice. It also encodes the <code>DeveloperBillingType</code>, so Google knows whether to treat the sale as an in app transaction or an external link transaction when it calculates the service fee. Where the token comes from depends on the scenario: in Scenario 1A it arrives through the <code>DeveloperProvidedBillingListener</code>, inside the <code>DeveloperProvidedBillingDetails</code> object, and in the other scenarios you generate it yourself with <code>createBillingProgramReportingDetails()</code>.</p>
<p>The report itself is a server side call to the Google Play Developer API, on the <code>externaltransactions</code> resource. After the user pays through your system, your backend calls <code>createexternaltransaction</code> with the token in the request body. The body differs by product type. A one time product uses a <code>oneTimeTransaction</code> wrapper, while a subscription uses a <code>recurringTransaction</code> wrapper that also declares the subscription type:</p>
<pre><code class="language-kotlin">{
  &quot;recurringTransaction&quot;: {
    &quot;externalTransactionToken&quot;: &quot;the_token_from_the_client&quot;,
    &quot;externalSubscription&quot;: { &quot;subscriptionType&quot;: &quot;RECURRING&quot; }
  }
}</code></pre>
<p>There is a distinction worth holding onto for subscriptions. The token is required for the first transaction in a recurring purchase, but renewals do not get a fresh token. Instead, you report each renewal against the original sale using <code>initialExternalTransactionId</code>, the identifier of that first transaction. The API also exposes <code>getexternaltransaction</code> to read a transaction back and <code>refundexternaltransaction</code> to report a refund, so the full lifecycle of an alternative sale is mirrored on Google’s side.</p>
<p>The service fee that results from these reports varies by transaction type and by geography, with the European Economic Area called out specifically, and the published rates live in Google’s external offers program documentation rather than in the Billing Library reference. The takeaway for your architecture is that choosing alternative billing does not remove the service fee. It moves the responsibility for declaring each sale onto your own backend.</p>
<h2><strong>Subscription upgrades and downgrades</strong></h2>
<p>Subscriptions add one more case: what happens when a user who already bought through your billing system upgrades or downgrades their plan. You do not want to show the choice screen again, because the user already made their choice on the original purchase, and Billing Choice preserves it.</p>
<p>You signal this by carrying the original transaction’s identifier into the replacement flow with <code>setOriginalExternalTransactionId()</code> on <code>SubscriptionUpdateParams</code> :</p>
<pre><code class="language-kotlin">val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder()
    .setBillingProgram(BillingProgram.BILLING_CHOICE)
    .build()

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(newPlanProductDetailsList)
    .setSubscriptionUpdateParams(
        BillingFlowParams.SubscriptionUpdateParams.newBuilder()
            .setOriginalExternalTransactionId(externalTransactionId)
            .build()
    )
    .enableDeveloperBillingOption(developerBillingOptionParams)
    .build()

billingClient.launchBillingFlow(activity, billingFlowParams)</code></pre>
<p>Because the original choice is preserved, no choice screen appears for the replacement. The user moves directly into the new plan through the billing system they already selected. Two identifiers are in play here, and they are not the same thing. <code>setOriginalExternalTransactionId()</code> runs on the client and names the prior purchase this upgrade replaces, which is what lets Google skip the choice screen. <code>initialExternalTransactionId</code> is a server side reporting field that chains a subscription’s renewals back to its own first transaction. So the new plan is still a new purchase: once the upgrade or downgrade completes, you report it as a new transaction with its own <code>externalTransactionToken</code>, and that transaction then becomes the <code>initialExternalTransactionId</code> its later renewals reference.</p>
<h2><strong>Designing a fair choice screen</strong></h2>
<p>When you render your own choice screen (the B scenarios), the UX guidelines stop being advice and become requirements you agreed to by declaring <code>DEVELOPER_RENDERED</code> in Play Console. The single principle behind every rule is that the user must be able to compare the two options fairly, with no thumb on the scale toward your billing system.</p>
<p>The concrete requirements:</p>
<ul>
<li><strong>Label who is responsible.</strong> Each option must clearly identify the authorized entity, app name, or developer name, so the user understands who fulfills the purchase and handles support.</li>
<li><strong>Show the right icons.</strong> Display the Google Play icon in full color against a neutral light or dark background per Google’s brand guidelines, and display only your own app icon or brand logo for your option. You may not dress your option in Google’s branding.</li>
<li><strong>Fetch payment method logos live.</strong> Use the assets from <code>getBillingChoiceInfoAsync()</code> to show Google Play’s payment methods, and treat your own payment methods the same way. You must fetch and display these each time and never cache or alter them. If you cannot determine your own payment methods, you may omit them, but you must still show Google Play’s.</li>
<li><strong>Keep the buttons equal and close.</strong> Button sizes, text size, font style, contrast, tap targets, and in-button logo sizing should be equivalent, and the two buttons must sit in close proximity so the user can compare them side by side.</li>
<li><strong>Use equivalent calls to action.</strong> The primary button labels must be equivalent and consistent, for example “Pay with Google Play” next to “Pay with [your service].” You may present differentiated offers, but if you do, Google Play’s loyalty benefits must be shown in an equivalent manner.</li>
<li><strong>Treat loyalty equally.</strong> If you display loyalty information, show it for both options with equal treatment, and pull Google Play’s loyalty message from <code>getBillingChoiceInfoAsync()</code> each time without caching it.</li>
<li><strong>Disclose the redirect.</strong> If your option sends the user to a website, you must clearly and prominently inform them they are leaving the app, with explicit wording such as “You’ll be redirected to a web page.”</li>
</ul>
<p>A nuance to note: the guidelines phrase the broad visual equality clause with “should,” while proximity, equivalent calls to action, in button logo sizing, and the no caching rules use “must.” When you are uncertain whether something is mandatory, the safe reading is to treat the equality requirements as binding, because the program’s entire purpose is to avoid nudging users away from Google Play Billing.</p>
<h2><strong>Supervised users and parental controls</strong></h2>
<p>One behavior is handled for you. For supervised users, Google Play automatically displays the appropriate parental control screen during the Billing Choice flow. The integration notes call this out for each of the four scenarios, so expect it whether Google renders the choice screen or you do. You do not build or trigger it yourself, so it is worth knowing the extra screen exists before it surprises you during testing with a supervised account.</p>
<h2><strong>What this means for your monetization</strong></h2>
<p>Stepping back from the API, Billing Choice is a trade. Google Play Billing handed you a great deal of infrastructure for its fee: payment processing, tax handling, receipts, refunds, renewals, fraud protection, and the entire reporting pipeline. Billing Choice lets you take some of that back, but only by taking on the work that came with it.</p>
<p>When a user picks your option, you own the payment processing, the fulfillment, the customer support, the refunds, and the server side reporting that keeps you compliant. The service fee does not vanish, it is assessed from the transactions you report through the <code>externaltransactions</code> API. And Google Play Billing remains on the screen as a first-class choice for every user, every time, which means your alternative has to earn its selection rather than being the only door.</p>
<p>That framing helps decide which scenario fits. If you mainly want to reduce friction without standing up a payment stack, the Google rendered scenarios (1A and 2A) carry the least integration burden, because Google draws the screen and mints the token. If you already run billing infrastructure on the web and want to route users there, an external link scenario (2A or 2B) reuses what you have. The fully developer rendered, in app scenario (1B) gives you the most control over the experience and demands the most from you in return: a compliant choice screen, your own payment flow, the disclosure dialog, and the reporting backend, all built and maintained by your team.</p>
<h2><strong>Conclusion</strong></h2>
<p>In this article, you’ve explored how Billing Choice reshapes the Android purchase flow into a fair choice between Google Play Billing and your own alternative. You saw the two decisions that produce the four integration scenarios, the <code>BillingProgram</code> API for enabling the program and reading its availability, how each scenario launches its purchase, the external transaction token that ties every alternative sale to a <code>createexternaltransaction</code> report, how subscription replacements preserve the user’s original choice, and the UX rules that govern a screen you render yourself.</p>
<p>Understanding these internals helps you make the decision Billing Choice actually asks of you, which is not “should I use alternative billing” but “which of the four scenarios matches what my team can build and operate.” The token flow, the reporting obligation, and the fairness requirements are the real cost of admission, and seeing them clearly up front is what prevents an integration that works in a demo but falls out of compliance in production.</p>
<p>Whether you are adding a single external link to an existing web checkout, building a full in app billing system behind your own choice screen, or simply letting Google render the choice while you collect payment, this foundation lets you reason about the program as a set of deliberate trade offs rather than a single switch to flip. Choose the scenario that fits, report every sale, and keep the choice honest.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Move your rating prompt out of onboarding — or risk rejection]]></title>
      <link>https://www.revenuecat.com/blog/engineering/dont-prompt-ratings-during-onboarding</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/dont-prompt-ratings-during-onboarding</guid>
      <pubDate>Thu, 18 Jun 2026 15:06:00 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Apple is rejecting apps that ask for ratings during onboarding. ]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/bd956ea8b2d7d12e7791c56b6fc7c1c30ef67c7c-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>A common pattern in many new and existing apps is to prompt users for a review during the onboarding process. However, next time you submit your app for review, Apple will reject it if you’re still using this pattern.</p>
<h2>Asking ratings too early is considered manipulation</h2>
<p>Apple has started rejecting apps under <a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guideline 5.6.3</a>, which states:</p>
<p><em>…Manipulating any element of the App Store customer experience such as charts, search, reviews, or referrals to your app erodes customer trust and is not permitted.</em></p>
<p>This isn’t a new guideline, but Apple hasn’t previously applied it to onboarding in this way. Prompting for a review early in the onboarding flow was a common, if questionable, tactic. Some apps were collecting thousands of five-star ratings before users had done anything meaningful with the app.</p>
<p>It is not clear if Apple will go through apps that are already in the App Store, but apps using this pattern will get rejected during the review process for new versions of the app. <a href="https://x.com/arielmichaeli/status/2067287069819355338">It might also be that Apple was already ignoring ratings that occur during onboarding</a> by comparing the time between install and rating; nullifying the ASO benefits.</p>
<h2>What should you do instead?</h2>
<p>The first step to fixing this is pretty simple: just remove all the rating and review prompts from your onboarding flows. The second step is more complicated; you should prompt only after the user has meaningfully engaged with your app. Ask for a review when there’s actually something to review. So when exactly?</p>
<p>Apple’s own Human Interface Guidelines say to prompt at “natural and happy moments”: after a user has completed something, achieved something, or had a genuine reason to form an opinion about your app.</p>
<p>That sounds straightforward, but in practice most apps don’t have a systematic way to know when that moment actually is. A user who just opened your app for the first time isn’t there yet. Neither is someone who made it through onboarding but hasn’t done anything meaningful.</p>
<p>Now, if you’re wondering how to do that, you’re in luck. <a href="https://www.revenuecat.com/blog/engineering/how-to-hack-your-app-store-ratings/">We’ve actually written about it before in this guide for ethically hacking your App Store ratings</a>. In the guide, you learn how to build “a happiness engine” that tracks user engagement with the app, and prompts for a review when you’re most likely to get a positive one. The same guide applies to the Google Play Store as well (although Google is not yet limiting).</p>
<h2>Bottom line</h2>
<p>Stop asking users to review your app during the onboarding flow. Apple will reject your app if they catch you doing it. Most likely they were already filtering out these ratings, by comparing the interval between app install and time of rating.</p>
<p>Instead prompt users to review the app after they’ve meaningfully engaged with it. Track those engagements and build an engine for prompting for a review when the user has both engaged with your app and is happy with their experience.</p>
<ul>
<li><a href="https://www.revenuecat.com/blog/engineering/how-to-hack-your-app-store-ratings/">How to hack your app store ratings (ethically)</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Monthly plans might be your best option]]></title>
      <link>https://www.revenuecat.com/blog/growth/monthly-subscriptions-when-to-offer</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/monthly-subscriptions-when-to-offer</guid>
      <pubDate>Thu, 18 Jun 2026 10:00:09 GMT</pubDate>
      <dc:creator><![CDATA[Daphne Tideman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[The hidden cost of long commitment periods]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/132c0102a08f131c73ec356d8a8e04a42702d642-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Everyone in subscriptions has an opinion on annual plans. <em>They’re better for LTV, better for cash flow, better for churn…</em></p>
<p>The data says so, apps love them, and honestly, the data isn’t wrong. Annual subscriptions across all categories perform better in terms of retention according to the <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps (SOSA) 2026 report</a>:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/80719a44203603aeb3acce2f856f38ee7c280cb8-840x539.png" alt="Annual subscriptions across all categories perform better in terms of retention according to the SOSA 2026 report:"/><figcaption>Annual subscriptions across all categories perform better in terms of retention</figcaption></figure>
<p>But I think we’ve overcorrected.</p>
<p><a href="https://www.revenuecat.com/blog/growth/annual-subscriptions-apps-pros-cons/#what-are-the-benefits-of-annual-subscriptions">Annual subscriptions definitely have their advantages</a>, but I think we are a bit mean to monthly subscriptions. I’m here to stand up for monthly subscriptions, give you the good and bad just like I’ve done for <a href="https://www.revenuecat.com/blog/growth/annual-subscriptions-apps-pros-cons/">annual subscriptions</a>, <a href="https://www.revenuecat.com/blog/growth/weekly-subscriptions/">weekly subscriptions</a>, and <a href="https://www.revenuecat.com/blog/growth/lifetime-subscriptions/">lifetime subscriptions</a> (sorry, monthly subscriptions, I didn’t mean to leave you until last).</p>
<p>It’s not that monthly plans are always better; it’s that there are specific situations where a monthly subscription will serve your app better than an annual one. And most founders never stop to ask which situation they’re actually in.</p>
<h2>First, retention and popularity don’t equal revenue</h2>
<p>Yes, annual plans retain better. Look at one-year retention across categories, and they win almost everywhere. Nobody is disputing that. In most categories, annual plans are the most popular option. According to <a href="https://www.revenuecat.com/state-of-subscription-apps/">SOSA 2026</a>:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/ad037bde512ac3455fda7850487c24a5788e96a4-893x436.png" alt="In most categories, annual plans are the most popular option. "/><figcaption>In most categories, annual plans are the most popular option.</figcaption></figure>
<p>Except in the productivity and gaming categories. But retention isn’t the same as revenue.</p>
<p>In most categories, monthly subscriptions generate revenue equal to or exceeding their share of subscriptions. Check out the revenue for monthly subscriptions in productivity:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/1dc8614fde4217700b7e7e045787ec5bceccac6e-900x413.png" alt="Revenue mix for monthly subscriptions across categories"/><figcaption>Revenue mix for monthly subscriptions across categories</figcaption></figure>
<p>The productivity category is the clearest example: 76.7% of subscriptions are monthly, but 90.7% of revenue comes from those monthly plans.</p>
<p>That gap exists because monthly subscribers typically pay more on a monthly basis, and while they do churn at higher rates, they’re also more likely to reactivate later on.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/b54f6bc09c051dfa2f7a95f88af969cd47a27b09-948x672.png" alt="Reactivation rate within 1 year by app category"/><figcaption>Monthly subs tend to pay more, churn more, but reactivate more</figcaption></figure>
<p>Your <a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/realized-ltv-per-paying-customer-chart">ARPPU</a> (Average Revenue Per Paying User) over time can actually be higher than you’d expect.</p>
<p>The reason the industry obsesses over annuals is that they create a reliable, predictable number on your dashboard. And that number feels good. But it’s worth asking: is that number telling you the truth?</p>
<h2>The uncomfortable truth about annual subscriptions</h2>
<p>Here’s something nobody says out loud: <strong>annual subscriptions can mask a broken product</strong>.</p>
<p>Someone who paid up front might not be using your app. They might regret the purchase. They might be planning to cancel at renewal. They may have stopped opening your app entirely, but your dashboard still shows “active subscriber” for another 9 months.</p>
<p><strong>That’s not retention. That’s delayed churn.</strong></p>
<p>And for early-stage apps, delayed churn is dangerous. It means you’re getting false signals about product-market fit. Your retention metrics look healthy. Your team feels good. And then renewal month arrives, and you discover the cliff you didn’t know was coming.</p>
<p>Monthly subscribers who stay are actively choosing you, every single month. That signal is worth more than an annual commitment made once, under the influence of a good discount and some well-timed push notifications.</p>
<h2>So when should you actually offer monthlies?</h2>
<p>There are five situations where monthly subscriptions can be your secret weapon.</p>
<h3>1. When you are still learning</h3>
<p>When someone commits to a quarter or a year, you don’t really know if they’re genuinely happy until renewal time. And if they’re not, you often won’t find out why until months later, by which point their memory has faded, and their feedback turns vague. You end up with things like “I stopped using it as much” or “I just found a better alternative for me.”</p>
<p>With monthly subscriptions, each renewal is an active decision, so users who stay are actively choosing you. Monthly subscriptions create faster feedback loops:</p>
<ul>
<li>You see churn patterns in weeks, not months</li>
<li>When someone leaves, you can ask why while the experience is still fresh</li>
<li>Users who stay are actively choosing you, giving you a cleaner signal on what’s working</li>
</ul>
<p>Yes, monthly churn tends to be higher, but that’s the point. You’re trading a vanity metric for something more valuable: speed of learning.</p>
<p>Why not just look at usage patterns? Identify who is or could be at risk? For startups, this can be challenging:</p>
<ul>
<li>A data infrastructure takes time and money to set up</li>
<li>You aren’t always sure what the best predictors of retention are</li>
<li>You don’t always know yet what normal usage patterns are to identify a drop in usage</li>
</ul>
<p>Meanwhile, monthly subscribers who stay are genuinely choosing you, repeatedly. That signal is worth more than an annual commitment made once under the influence of a good discount and some great marketing. Think about what you can learn from monthly subscribers:</p>
<ul>
<li><strong>Who’s churning?</strong> Is it a specific customer segment? A specific acquisition channel?</li>
<li><strong>When are they churning?</strong> Month 1 (onboarding issue), month 2-3 (habit formation), or later (ongoing value).</li>
<li><strong>What do they say when they leave?</strong> You can ask while it’s fresh.</li>
</ul>
<p>Start with offering monthlies if you are still learning whether you have strong retention and what drives it. It’ll help you learn what drives product-market fit far faster and has an extra benefit for startups where trust is low.</p>
<p>For some brands, offering only monthly (or making it the default) for the first 6-12 months could dramatically accelerate <a href="https://www.revenuecat.com/blog/growth/learning-roadmap-for-apps/">the rate at which you learn what’s working</a>.</p>
<h3>2. When trust is low</h3>
<p>For startups, you don’t have a huge number of reviews or well-known individuals shouting “use this app” to the world (well, most don’t). So monthly subscriptions can be an easier first step than annual plans as you build trust, refine onboarding, and get sharper at communicating value.</p>
<p>In that context, asking someone to commit to a year upfront is a big ask. Monthly plans lower the barrier significantly.</p>
<p>Even with a free trial, I consistently see users default to monthly first — particularly in categories where “will I actually use this?” is a real question, like productivity, health and fitness, and education. They try it, and if it sticks, they upgrade to an annual later.</p>
<p>This is also worth thinking about for <a href="https://www.revenuecat.com/blog/growth/web-to-app-funnels/">web-to-app flows</a>.</p>
<p>When a user hasn’t seen your app yet — they’ve come through a web landing page, they don’t know the product — the commitment ask is even higher. App stores provide a layer of trust: standardized billing, the familiarity of Apple or Google Pay, and that quick double-click checkout. On the web, that safety net is thinner. In that context, a monthly subscription becomes an easier first step.</p>
<p>There’s another benefit here, too: more conversions means more data. More signals to test and optimize your funnel with. Every monthly subscriber who converts is a learning opportunity that an unconverted annual prospect simply doesn’t give you.</p>
<h3>3. When your users prefer it</h3>
<p>Not every user wants to pay for a year upfront. For some, it’s a financial preference. For others, it’s cultural.</p>
<p>In MEA and Asia-Pacific, monthly plans are notably more popular than in Western markets.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/c63f60eb9c702db5229b0dc7a8112ca3832d34c1-951x669.png" alt="Plan mix by geography "/><figcaption>In MEA and Asia-Pacific, monthly plans are notably more popular than in Western markets.</figcaption></figure>
<p>The preference for pay-as-you-go and <a href="https://www.webintravel.com/apac-at-the-heart-of-worldlines-ambitions-to-enter-high-growth-markets">digital wallet payments</a> makes monthly subscriptions a more natural fit. And it’s not necessarily about price sensitivity, it’s about how people prefer to manage money in those regions.</p>
<p>Geography isn’t the only factor that comes into play. Certain generations prefer shorter subscriptions. We saw this with weekly subscriptions. <a href="https://www.pymnts.com/earnings/2023/tinder-taps-short-term-subscriptions-to-tackle-gen-zs-commitment-phobia/">Gen Z seems to prefer them due to a higher commitment phobia</a> (my favorite thing about this research was that it was done by Tinder… ironic for a dating app). So, certain generations may also be more comfortable with a monthly subscription than with an annual one. It seems Gen Z and younger millennials (under 34) value shorter subscriptions with greater flexibility, while older generations tend to prefer annual subscriptions for stability and discounts.</p>
<p>If your audience skews younger or you’re building for emerging markets, defaulting to annual may actually be suppressing your conversion rate.</p>
<h3>4. When your use case suits it</h3>
<p>We all love the idea of users using our apps forever and forever, but sometimes it isn’t needed.</p>
<p>Take therapy, when I used BetterHelp, a therapy platform, I never for a second considered an annual subscription. Costs aside, I hoped I wouldn’t need it for a full year, and that proved true. After 3-4 months, I stopped using the platform, happily churning.</p>
<p>Another great example is Hinge, the dating app, which literally advertises that they are designed for short-term usage:</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/31b1a2a9d3616ee2c80f5b177c90e081d50a7032-1200x1800.png" alt="Another great example is Hinge, the dating app, which literally advertises that they are designed for short-term usage."/><figcaption>Source: Creative Review</figcaption></figure>
<p>You need to think about your users’ expected use case, and whilst you can extend it or grow with them, for some users, you might just be a short-term fix, and that is okay.</p>
<p>Some apps are genuinely short-term solutions:</p>
<ul>
<li>Language learning before a trip.</li>
<li>A fitness app to prep for a specific event</li>
<li>A productivity tool for a project that has an end date</li>
</ul>
<p>Trying to lock those users into annual subscriptions doesn’t increase their lifetime value; it increases their likelihood of feeling trapped and leaving a negative review.</p>
<p>Think honestly about your users’ expected use case. If your core value proposition is a transformation that happens over three to six months, a monthly (or quarterly) subscription might actually serve both parties better than an annual one.</p>
<p>Simpler is usually better. Look at your average retention and choose two subscription durations that match your actual use case, rather than offering every option at once.</p>
<h3>5. When you want to drive revenue up, not just retain users</h3>
<p>This one surprises people. Usually, for most apps, even <a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/realized-ltv-per-paying-customer-chart">unbounded ARPPU</a> is lower for monthly subscriptions.</p>
<p>Yet, Spotify doesn’t offer annual subscriptions. Netflix doesn’t either. These are two of the most successful subscription businesses on the planet, and they’ve built their entire model on monthly billing.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/9f0984b969957eb99ca27535eaa674bf2e6019c1-1220x1088.png" alt="Spotify's plan mix - no annual subscriptions"/><figcaption>Spotify doesn’t offer annual subscriptions</figcaption></figure>
<p>Part of that is product complexity — layering monthly/annual on top of individual/duo/family plans creates more confusion than value. But I think there’s something more interesting going on.</p>
<p>Monthly-only pricing works when you have two things simultaneously: high early churn and very stable long-term retention. The users who stay past a certain point become genuinely sticky — content keeps updating, the habit is formed, and the switching cost is real. For those users, not having an annual discount option means they pay full price every month, indefinitely. Your LTV on retained users actually goes up.</p>
<p>But Peloton is an interesting case study for this. When I <a href="https://www.revenuecat.com/blog/growth/peloton-retention-takeaways/">canceled my Peloton subscription</a>, I was frustrated by the lack of an annual option — the monthly price felt hard to justify. But Peloton didn’t need to win that battle, because the hardware investment locks users in and makes them willing to pay a premium monthly rate anyway.</p>
<p>The lesson isn’t “don’t offer annuals.” It’s about understanding what actually creates stickiness in your product before you default to discounting your way to retention.</p>
<p>Some of these apps may initially offer an annual subscription option and then, over time, slowly shift back to monthly once they’ve maximized revenue and retention.</p>
<h2>How do you decide what to offer?</h2>
<p>Most apps I work with default to annual as the primary option without ever asking: Does this actually match where we are right now?</p>
<p>Here’s the diagnostic I’d suggest:</p>
<p><strong>Offer monthly as your primary option (or only option) if:</strong></p>
<ul>
<li>You’re pre-PMF or still learning what drives retention</li>
<li>Your brand is new, and trust is still being built</li>
<li>Your primary market is MEA, APAC, or your audience skews under 34</li>
<li>Your use case has a natural endpoint within 3–6 months</li>
<li>Your conversion rate is low, and you need more data to optimize the funnel</li>
</ul>
<p><strong>Lead with annual, but keep monthly available if:</strong></p>
<ul>
<li>You have strong evidence of what drives long-term retention</li>
<li>Your product creates a genuine long-term habit</li>
<li>Your audience is used to SaaS-style annual commitments (productivity, B2B-adjacent tools)</li>
<li>You’re confident in your onboarding and activation — users who start are getting to value</li>
</ul>
<p><strong>Consider exclusively monthly if:</strong></p>
<ul>
<li>You have exceptionally stable long-term retention and don’t want to discount it away</li>
<li>Your product model naturally supports it (premium content, ongoing service)</li>
<li>You’re deliberately running a lean paywall to maximize learning before scaling</li>
</ul>
<h2>If you’re currently offering only annual, how can you actually test this?</h2>
<p>So you’ve read all of this, and you’re thinking: fine, maybe I should give monthly a shot. You kind of have a point, Daphne.</p>
<p>But how do you do it without tanking your metrics?</p>
<p>A few things to consider before you start:</p>
<h3>1. Define what success looks like <em>before</em> you start</h3>
<p>If you test monthly and see higher conversion but lower ARPPU in the short-term, is that a win or a loss? The answer depends on where you are.</p>
<p>For an early-stage app still learning retention, more converted users and faster feedback loops might be worth more than a higher per-subscriber ARPPU number right now.</p>
<p>Know what you’re optimizing for before you interpret the results.</p>
<h3>2. Don’t just add monthly and hope for the best</h3>
<p>The most common mistake is simply bolting a monthly option onto a paywall that was clearly designed for an annual plan. Where you place each option and how you promote it should depend on your goal.</p>
<p>If you’re trying to learn, and your paywall currently defaults to annual with a prominent “save X%” badge, then adding a small, secondary monthly option at the bottom won’t tell you much at all.</p>
<p>If, instead, you’re trying to meet users where they are — or reduce friction in low-trust environments — then monthly needs to be more visible and genuinely considered. You can still position annual as the better value on a per-month basis, but monthly has to get a fair test if you want meaningful insights from it.</p>
<h3>3. Think carefully about trial interaction</h3>
<p>A <a href="https://www.revenuecat.com/blog/growth/should-your-app-stop-offering-free-trials/">free trial</a> plus a monthly plan is a very different psychological ask from a free trial plus an annual plan. It reduces perceived risk even further, but it can also lead to lower-quality subscribers.</p>
<p>For that reason, some teams deliberately choose not to include a free trial on the monthly tier.</p>
<p>If your trial-to-paid conversion is lower than you’d like, testing a monthly entry point is one of the cleanest ways to diagnose whether the issue is price sensitivity or commitment aversion. In some cases, you might even start by offering a trial on the monthly plan first, and then remove it later once you understand how users behave.</p>
<h3>4. Consider the monthly-to-annual upgrade path</h3>
<p>One of the strongest arguments for offering a monthly plan isn’t the plan itself — it’s what happens after. Users who start on a monthly plan, experience real value, and then upgrade to annual are often your highest-quality subscribers. They’ve effectively chosen you twice.</p>
<p>If you can design a well-timed upgrade nudge — whether that’s in-app, after a key activation moment, or around the one-month mark — monthly stops being a “discounted alternative” and becomes a lower-friction entry point that feeds into annual revenue over time.</p>
<p>You also want to keep in mind that reactivation data, <a href="https://www.revenuecat.com/webinars/win-back-webinar-2024/">getting monthlies to reactivate</a>, works better when you have the lifecycle marketing in place to support it.</p>
<h2>Give monthly subscriptions a consideration, please</h2>
<p>Annual subscriptions feel like a win: higher LTV, stronger retention curves, cash upfront. It’s easy to see why teams default to them.</p>
<p>But too many apps push annual plans before they’ve actually earned the right to ask for that level of commitment — before they really understand whether retention is durable, and before they’ve built enough trust for a 12-month ask to feel safe rather than risky.</p>
<p>Monthly subscriptions aren’t just a concession to users who won’t commit. In the right context, they’re a more deliberate product decision: one that gives you cleaner data, lower conversion friction, and faster feedback loops that help you build something people genuinely want to keep paying for, month after month.</p>
<p>Give them a chance – not as a fallback, but as a deliberate choice.</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[He turned down 75K for his app with 12K in sales. It hit $1M two years later.]]></title>
      <link>https://www.revenuecat.com/blog/growth/eric-duffett-shot-pattern-launched-podcast-2026</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/eric-duffett-shot-pattern-launched-podcast-2026</guid>
      <pubDate>Wed, 17 Jun 2026 13:17:02 GMT</pubDate>
      <dc:creator><![CDATA[Charlie Chapman]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Eric Duffett had done $12,000 in total sales when a company offered $75,000 for his side-project golf app — and told him they'd crush him if he said no.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/9a18a4e5cfe31e5a3d311f741c73ad8704a4c3a3-1600x840.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<h2><strong>The $5,000 bet that changed everything</strong></h2>
<p>Shot Pattern started as a hobby. Eric Duffett had just stepped down from coaching basketball because his second child was on the way, and he sat still for about five seconds before opening his laptop. He wanted to see if he could build a simple golf tool — one that would draw two lines on a satellite map showing the width of his landing area. No golf course data, no hole layouts. Just lines on Apple Maps.</p>
<p>It was $20 a year. People would download it, open it at their house, and see it pointing at their backyard. “That’s surprising,” Eric says. But on the golf course, it worked — like a compass for shot strategy.</p>
<p><a href="https://www.youtube.com/watch?v=wXDqhhSNJQ4">Watch on YouTube</a></p>
<p>Within two months, something felt different. Downloads and trials were coming in without Eric understanding where they came from. The reason: serious golfers were already doing this exact thing manually on Google Maps, measuring fairway widths with the built-in ruler tool. Eric had automated a workflow that already existed in a community of strategy-obsessed players.</p>
<p>Then he hit a wall. To make the app actually useful — to give it awareness of golf courses, holes, and hazards — he needed to buy course data. The only option was a $5,000 upfront API purchase. He’d made about $1,000 in total sales. His wife asked the obvious question: “Is this a hobby or is this a business?”</p>
<p>He paid the $5,000. And buried in that API data, he found something he didn’t know he’d purchased: geo-point outlines of every fairway, bunker, water hazard, and green. That was the missing piece for running shot simulations — the “moneyball” feature that would define the app.</p>
<h2><strong>“This feature is highly illegal”</strong></h2>
<p>Eric built a half-finished prototype of the simulation feature, took a screenshot, and posted it on Twitter: “Hey, this feature is highly illegal if you were to use it during tournament play, but it’s coming soon to Shot Pattern.”</p>
<p>That single tweet was the first time anything he posted got real momentum with a cold audience. Phone calls started coming in from golf influencers and coaches. “Oh, now we’re off to the races,” he says. “Now we have something.”</p>
<p>Shortly after, he got a different kind of phone call. A respected golf company — people Eric admired, “man-crush in the golf world” level — said they’d used the app, loved it, and wanted to buy it. They’d been thinking of building something similar. “What’s your number?”</p>
<p>Eric asked for $250,000. He’d done $12,000 in total sales. They countered at $75,000 — with a non-compete clause thrown in at the very end. No more golf apps, ever.</p>
<p>He said no.</p>
<h2><strong>The dark winter</strong></h2>
<p>The high lasted about two weeks. Then Eric fell into what he describes as a “dark, dark hole.” He’d turned down real money — not life-changing, but a cushion for a family with a newborn — and the buyers had made their position clear: “We’re going to build what you’ve built. We have more distribution, more brand power. We have a full-time developer. You’re teaching and you have a newborn. It’s over for you.”</p>
<p>October hit. Golf season ended. Revenue dropped. Trials dropped. Subscriptions dropped. Everything went backwards. Eric did his taxes and calculated that at his current profit rate, it would take 75 years to catch up to the $75,000 he’d walked away from.</p>
<p>He tried to sell the app to someone else. Nobody responded. He emailed Curtis Herbert, who talked him off the ledge.</p>
<h2><strong>Spend a dollar, make 4</strong></h2>
<p>The next spring, Eric decided to try Meta ads. He’d learned from Sub Club and other podcasts that you need creative inventory and enough budget for the algorithm to learn. He paid some content creators, set a $300/day budget, and at the last second threw in a scrappy screen recording with his own voiceover — raw, unpolished, zero production cost.</p>
<p>That last-second addition outperformed everything else. Cost per install: $0.80. His 30-day LTV per download: over $4. “Spend a dollar, make four,” he says. He told his wife he was going to get a line of credit at the bank. She grabbed him by the collar.</p>
<p>The reason it worked wasn’t sophisticated targeting. The creative itself did the filtering. It showed the app doing the thing that strategy-obsessed golfers already cared about, in language they already used. People who weren’t in that niche scrolled past. People who were stopped in their tracks.</p>
<p>By the end of 2024 — his first full year — Eric had outrun the $75K offer. Not just in revenue, but in profit and what he paid himself. He still had the app. He still had the momentum.</p>
<h2><strong>$1M as a teacher with a side project</strong></h2>
<p>In early 2026, Shot Pattern crossed $1 million in total sales. Eric is still a full-time high school teacher. He teaches intro to business, personal finance, and next year, sports marketing. He recently showed his students a viral video he’d made for the app and explained his target customer profile: “They look like me, they dress like me, they probably think like me, they’re bald like me.”</p>
<p>A student raised his hand, patted his belly, and said he was working on looking more like Eric’s target customer. “He didn’t mean anything by it,” Eric says, “but it came off as quite the dis.”</p>
<p>The competition that threatened to crush him never built the feature. Eric hired his first contractor — not a developer, but a general business operator to handle email automation, paywalls, and experiments. He’s still the sole developer. He still loves that part.</p>
<p>“I still think it’s one of the coolest things,” he says. “To sit down and solve this puzzle with your mind and then show it to people and give them value in exchange for dollars and have it feel like a win for everybody.”</p>
<p>In <a href="https://www.youtube.com/watch?v=wXDqhhSNJQ4">the full episode</a>, Eric also talks about his first failed meditation app for athletes, what he learned from working with UI/UX students, how he handles the emotional weight of seasonal revenue swings, and why he recommends the book The Only Way to Win by Dr. James Loehr to every high performer he meets.</p>
<h2>Guest links:</h2>
<p>•<a href="https://www.linkedin.com/in/eric-duffett/">Eric Duffett on LinkedIn</a></p>
<p>•<a href="https://twitter.com/shotpattern">Shot Pattern on social media</a> (all platforms)</p>
<p>•<a href="https://apps.apple.com/app/shot-pattern-golf-gps/id6449382487">Shot Pattern app</a></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Why we removed “always decline” as a refund preference]]></title>
      <link>https://www.revenuecat.com/blog/company/why-we-removed-always-decline-refund-preference</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/company/why-we-removed-always-decline-refund-preference</guid>
      <pubDate>Wed, 17 Jun 2026 13:06:54 GMT</pubDate>
      <dc:creator><![CDATA[Perttu Lähteenlahti]]></dc:creator>
      <category><![CDATA[Company]]></category>
      <description><![CDATA["Always decline" refunds is gone from RevenueCat. Here's why it was working against you.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/314ee87248822f9029b34ad5f87ea91596472671-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>If you’ve recently tried to set your refund preference to “Always decline” in the RevenueCat dashboard, you may have noticed the option is gone. We removed it because blanket declines lose their effectiveness over time and can push users into chargebacks – the worst outcome for everyone. If “Always decline” was your existing setting, it’s still active. Below: the full reasoning and how it affects you.</p>
<aside class="tip"><strong>Update</strong><p>RevenueCat now includes <strong>Refund Control</strong>, a dedicated dashboard for configuring refund preferences across different customer segments. Instead of relying on a single global preference, you can define policies for different audiences and let Apple and Google receive the most appropriate recommendation for each refund request. <a href="https://www.revenuecat.com/docs/customers/refund-control">See our documentation for details</a>.</p></aside>
<h2>What changed</h2>
<p>The RevenueCat dashboard no longer allows users to select “Always decline” as a global refund preference.</p>
<figure><img src="https://cdn.sanity.io/images/c3qnx9b0/production/2d998fec0b8b9cf956226259c5122b1c1930e75d-1806x790.png" alt="Always prefer declining refunds is no longer an option in the Handling of refund requests section."/><figcaption>‘Always prefer declining refunds’ is no longer an option in the Handling of refund requests section.</figcaption></figure>
<p>The available options now are:</p>
<ul>
<li><strong>Let Apple decide:</strong> Neutral, no preference expressed</li>
<li><strong>Grant in full:</strong> You prefer the refund is approved</li>
<li><strong>Grant prorated:</strong> You prefer a partial refund based on consumption</li>
</ul>
<p>We didn’t unilaterally change settings for developers who already had “Always decline” enabled — it’s still active if that was your setting. If you do switch it off, you’ll see a confirmation prompt letting you know you won’t be able to re-enable it. That’s your call to make, but the case for keeping it on is getting weaker.</p>
<h2>Why “always decline” was removed</h2>
<p>This comes down to how Apple’s refund system actually works:</p>
<p><strong>Refund preferences are signals, not instructions.</strong> When you set a refund preference and send it to Apple via the <a href="https://developer.apple.com/documentation/appstoreserverapi">App Store Server API</a>, you’re not making a binding decision. <a href="https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest">Apple uses your input as one of many factors when evaluating a refund request</a>. The <code>refundPreference</code> field in a <code>ConsumptionRequest</code> is advisory by design.</p>
<p><strong>Blanket declines erode their own effectiveness.</strong> When a preference is always set to “decline” regardless of context — whether the user barely touched the app or encountered a genuine bug that made it unusable — Apple’s systems start to discount it. A preference that clearly doesn’t account for the specifics of a transaction carries less weight over time. The signal becomes noise.</p>
<p><strong>Rejecting valid refund requests can backfire.</strong> When a legitimate refund request is declined, some users go straight to their bank and issue a chargeback instead. Chargebacks are worse for developers, as a high chargeback rate puts your developer account at risk of being suspended by Apple. A blanket “always decline” policy can inadvertently push users toward the worse outcome for everyone.</p>
<h2>What to do right now</h2>
<p>If you want more control over refund recommendations, use Refund Control in the RevenueCat dashboard.</p>
<p>Refund Control lets you create multiple policies based on customer audiences, such as platform, country, first purchase date, or any custom audience you define. Each policy can express your preferred refund outcome:</p>
<ul>
<li>Decline</li>
<li>Grant</li>
<li>Neutral (no preference)</li>
</ul>
<p>RevenueCat sends the matching preference to Apple through the Consumption Request API and to Google through the Refund Review API. In both cases, your preference is advisory only—the store always makes the final refund decision.</p>
<p>This approach encourages nuanced refund policies instead of blanket &quot;decline everything&quot; behavior, which app stores have made clear they discourage. It also lets you reserve decline recommendations for cases where they're actually appropriate, making the signal more meaningful over time</p>
<h2>The bottom line</h2>
<p>“Always decline” was a blunt instrument, and it was becoming less effective over time. We’ve seen enough cases of it backfiring that we felt it was important to be straight with you about this. We know it’s not what everyone wanted to hear. At the end of the day, a setting that doesn’t do what you think it does isn’t worth the risk.</p>
<p><strong>See also:</strong></p>
<ul>
<li><a href="https://www.revenuecat.com/docs/refunds">How to handle refunds in RevenueCat</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Launch Party June ’26: The New Features You Should Know]]></title>
      <link>https://www.revenuecat.com/blog/engineering/launch-party-june-26-the-new-features-you-should-know</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/launch-party-june-26-the-new-features-you-should-know</guid>
      <pubDate>Fri, 12 Jun 2026 00:46:12 GMT</pubDate>
      <dc:creator><![CDATA[Francie Fernandes]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[A recap (and videos) of our latest launch party demos.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f267ce415f3792d24d538314b35272ae7810485a-1642x872.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>On June 5, 2026 we hosted special edition of our <a href="https://app.livestorm.co/revenuecat/live-revenuecat-demo?type=detailed">bi-weekly office hours</a> featuring live demos and Q&amp;A around our recent features launches. It was hosted by the Product Managers who built and shipped the features, giving you the chance to discuss your questions with the team live.</p>
<p>If you couldn’t join live, here’s a summary of what we covered and clips of the demos.</p>
<h2>Paywalls</h2>
<h3>AI Editor</h3>
<p><a href="https://www.youtube.com/watch?v=Rdqbfgk3N5s">Watch on YouTube</a></p>
<p>The AI Editor is a conversational AI agent built directly into the RevenueCat Paywall Editor that lets you create, edit, and refine paywalls using natural language prompts.</p>
<p>This feature is ideal for rapidly generating a production-ready paywall from scratch, adjusting visual design like colors and typography, or updating existing templates without manual tweaking. You chat with the AI (for example, “Make a paywall for a book tracking app targeting the BookTok community”) and it automatically adjusts the copy, layout, and imagery. It can also accept a <code>design.md</code> file to match your brand guidelines or a screenshot for inspiration.</p>
<p>To set it up, go to Paywalls in the RevenueCat dashboard, click <strong>Create paywall</strong>, and select <strong>Generate with AI</strong>, or open an existing paywall and use the <strong>AI Editor</strong> tab in the left sidebar. <a href="https://www.revenuecat.com/blog/company/paywalls-ai-editor/?_gl=1*awxc43*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.">Check out the docs here.</a></p>
<h3>Paywall Rules</h3>
<p><a href="https://www.youtube.com/watch?v=o6pfogF3ebw">Watch on YouTube</a></p>
<p><a href="https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls/rules">Paywall Rules</a> allow you to customize the visibility of paywall components based on both preset and Custom Variable based rules, enabling you to customize a single paywall to support multiple scenarios.</p>
<p>One example is using Paywall Rules to show a trial timeline only when a trial is available, or to display a different package (like family sharing vs. individual) based on a custom variable.</p>
<p>Rules are evaluated at runtime. You define conditions such as <code>offer.intro</code>, <code>package.identifier</code>, or custom variables, and set components to be visible or hidden when those conditions are met.</p>
<p>To set this up, open the <strong>Paywall logic</strong> tab in the Paywall Editor, create a new rule based on your desired condition, and then set the visibility of your paywall components for that rule. Check out <a href="https://www.revenuecat.com/blog/engineering/announcing-paywall-rules-show-or-hide-paywall-components/">this blog post</a> for more info.</p>
<h2>Funnels</h2>
<h3>Funnels Builder</h3>
<p><a href="https://www.youtube.com/watch?v=B5ayNuAhOOc">Watch on YouTube</a></p>
<p>Funnels allow you to build customizable, hosted web onboarding experiences that can be designed remotely from the RevenueCat dashboard and shipped by anyone on your team.</p>
<p>Use cases for funnels include web-to-app acquisition funnels for ad campaigns or influencers, surveying users before checkout, and capturing web conversions with lower friction. You build multi-step web flows using the Funnels editor, which is similar to the Paywalls editor. You can add screens, branching logic, survey questions, and a checkout step. It currently supports Stripe and Paddle for payments.</p>
<p>To set it up, configure a payment provider in RevenueCat, create a Funnel in the dashboard, design your steps and branching logic, and deploy the generated URL. <a href="https://www.revenuecat.com/docs/tools/funnels/creating-funnels">Check out the docs here.</a></p>
<h3>A/B Testing</h3>
<p>Testing within Funnels enables you to run experiments directly within your web Funnels to optimize conversion.</p>
<p>This feature is used for testing different price points, paywall designs, or onboarding survey flows to see which yields the highest conversion rate on the web. In the Funnel Editor, you can add an “Experiment” branch that splits traffic between different paths (e.g., a high-price paywall vs. a low-price paywall) and track the results in the funnel analytics.</p>
<p>To set it up, add an Experiment node in your funnel flow and connect it to your variant screens or paywalls. <a href="https://www.revenuecat.com/docs/tools/funnels/experimenting-with-funnels">Check out the docs here.</a></p>
<h2>Web</h2>
<p><a href="https://www.youtube.com/watch?v=Q_xAWmOzsYg">Watch on YouTube</a></p>
<h3>One-Tap Purchases via Express Checkout</h3>
<p>One-Tap Purchases via Express Checkout adds Apple Pay and Google Pay buttons directly to your web paywalls and funnels for frictionless purchasing.</p>
<p>This maximizes web conversion rates by allowing users to skip the traditional credit card form and check out with a single tap. The Express Checkout component surfaces Apple Pay or Google Pay (if supported by the user’s device and browser) and processes the payment through Stripe.</p>
<p>To set it up, add the “Express checkout buttons” component to your paywall in the Paywall Editor or Funnel Editor. <a href="https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls/components?_gl=1*1q9msjx*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.#express-checkout">See the docs here.</a></p>
<h3>Stripe Billing &amp; Stripe Managed Payments</h3>
<p>RevenueCat supports both Stripe Billing and Stripe Managed Payments, a Merchant of Record solution where Stripe handles tax compliance, collection, and remittance.</p>
<p>A merchant of record solution is ideal for selling subscriptions on the web while offloading the complexity of global sales tax, VAT, and GST to Stripe. RevenueCat creates a Stripe Checkout session, and for eligible products, Stripe acts as the Merchant of Record, processes the payment, handles taxes, while RevenueCat tracks the transaction and unlocks entitlements.</p>
<p>To set it up, connect your Stripe account via the RevenueCat Stripe App, create a Stripe Web Config in RevenueCat, and toggle on “Use Managed Payments when available”. <a href="https://www.revenuecat.com/docs/web/integrations/stripe?_gl=1*o7m74e*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.">See the docs.</a></p>
<h3>Discounts and Discount Codes</h3>
<p>This is a flexible system to apply percentage-based discounts to products in RevenueCat Web Billing using shareable, customer-facing discount codes.</p>
<p>Use cases include running promotional campaigns like Black Friday, attributing sales to influencers via unique codes, or offering win-back discounts. You create a discount (e.g., 50% off for 3 months) and generate codes (e.g., BLACKFRIDAY50). Customers enter the code at checkout, or it can be auto-applied via a URL parameter on a Web Purchase Link.</p>
<p>To set it up, navigate to <strong>Product catalog &gt; Web discounts</strong> in the dashboard, create a discount, define its rules and codes, and enable discount codes on your Web Purchase Links. <a href="https://www.revenuecat.com/docs/web/web-billing/discounts?_gl=1*awxc43*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.">Check out the docs here.</a></p>
<h2>Experiments</h2>
<h3>pLTV Winner, Credible Intervals, Astra Analysis, Winner Rollout</h3>
<p><a href="https://www.youtube.com/watch?v=eeaffHtHlbE">Watch on YouTube</a></p>
<p>This suite of features enhances Experiments with advanced statistical tools: predicted LTV (pLTV) winner predictions, credible intervals for metrics, AI-driven analysis via Rico (Astra), and one-click winner rollouts.</p>
<p>These tools help you make confident decisions on A/B tests by understanding not just initial conversion, but long-term value, and quickly deploying the winning variant. RevenueCat calculates the Chance to Win and 95% credible intervals for conversion metrics. Once sufficient data is gathered, it predicts the pLTV winner. You can ask Rico to analyze the nuances of the test, and use the “Rollout” option to set the winner as the default offering or create a targeting rule.</p>
<p>These features automatically populate in the Results tab of your running Experiments.</p>
<h2>AI Tools</h2>
<h3>Rico</h3>
<p><a href="https://www.youtube.com/watch?v=IRKJqWe__XQ">Watch on YouTube</a></p>
<p>Rico is RevenueCat’s AI-powered app growth advisor built into the dashboard and available in Slack.</p>
<p>It is designed for diagnosing revenue drops, analyzing experiment results, benchmarking against industry data, or answering SDK integration questions using plain language. Rico has access to your app’s data, charts, experiments, RevenueCat documentation, and industry benchmarks. You ask it a question, and it reasons through the data to provide an answer.</p>
<p>To set it up, use the chat interface in the dashboard overview. To use it in Slack, go to Account Settings, select the Rico tab, and connect it to your workspace.</p>
<p><a href="https://www.revenuecat.com/blog/company/rico-app-growth-advisor/?_gl=1*o7m74e*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.">Read more on Rico in this blog post.</a></p>
<h3>AI Toolkit</h3>
<p><a href="https://www.youtube.com/watch?v=Nz1xLbVgRQ4">Watch on YouTube</a></p>
<p>RevenueCat offers a comprehensive AI toolkit to help AI agents set up RevenueCat, integrate it into apps, and access your revenue data — all directly from your AI coding assistant.</p>
<p><a href="http://revenuecat.com/docs/tools/overview">Explore the docs.</a></p>
<h2>Charts &amp; Dashboard</h2>
<h3>Benchmarks</h3>
<p><a href="https://www.youtube.com/watch?v=jn1NtBKww64">Watch on YouTube</a></p>
<p>Benchmarks allow you to compare your app’s key metrics like conversion, churn, and realized LTV against other apps in the same store and category.</p>
<p>This helps identify growth opportunities, set meaningful goals, and see how your app stacks up against peers in metrics like trial conversion or refund rates. RevenueCat calculates benchmark percentiles using anonymized data from apps in the same category over the last 12 months. Your app’s performance is shown as a percentile (e.g., 90th percentile).</p>
<p>Benchmarks are automatically available in the dashboard under the Benchmarks tab for eligible apps.</p>
<p><a href="https://www.revenuecat.com/docs/dashboard-and-metrics/benchmarks?_gl=1*nnzjuy*_up*MQ..*_ga*NjI4MTgwNDM0LjE3ODEyMjUyMTM.*_ga_0MLNVKXFGB*czE3ODEyMjUyMTMkbzEkZzAkdDE3ODEyMjUyMTMkajYwJGwwJGg1MTcxNzAwNjU.">See the docs.</a></p>
<h3>In-App Ad Revenue in Charts</h3>
<p><a href="https://www.youtube.com/watch?v=nVQTdLVnZbI">Watch on YouTube</a></p>
<p>In-App Ad Revenue in Charts tracks ad impressions, clicks, and revenue alongside your subscription data for a unified view of your app’s hybrid monetization.</p>
<p>This is essential for calculating accurate Realized LTV that includes both ads and subscriptions, and understanding how ad revenue varies by segment. By hooking into your ad mediation platform’s (like AdMob or AppLovin) impression-level revenue data (ILRD) callbacks, RevenueCat tracks ad events and displays them in dedicated Ad Charts and the main Revenue mix chart.</p>
<p>To set it up, opt-in to the beta in the Ads page of the dashboard, and integrate the AdMob Adapter SDK or manually call the <code>AdTracker</code> methods from your ad SDK callbacks. <a href="https://www.revenuecat.com/docs/dashboard-and-metrics/charts/ads">See the docs.</a></p>
<h2>Store Updates</h2>
<h3>Apple 12-Month Commitments</h3>
<p><a href="https://www.youtube.com/watch?v=mXZhEWSvazE">Watch on YouTube</a></p>
<p>This feature supports Apple’s new subscription option that bills customers monthly but requires a 12-month commitment.</p>
<p>It is highly effective for lowering the upfront price barrier for annual subscriptions in price-sensitive regions while maintaining long-term retention. Apple models this as a billing plan under an annual product. RevenueCat exposes the monthly commitment plan as a separate product ID with a <code>:monthly </code>suffix (e.g., <code>com.myapp.product:monthly</code>).</p>
<p>To set it up, configure the billing plan in App Store Connect. In RevenueCat, create a second product and check the “12 months commitment” option. Ensure you are using StoreKit 2 and iOS 26.4+.</p>
<p><a href="https://www.revenuecat.com/blog/engineering/monthly-subscription-12-month-commitment/">More on the blog.</a></p>
<h2>Join our next office hours</h2>
<p>Wish you were there? Don’t worry, you can find our team live every other <a href="https://app.livestorm.co/revenuecat/live-revenuecat-demo?type=detailed">Friday for Office Hours.</a></p>
<p>We gather members of the RevenueCat team, our community, and anyone curious about RC or consumer-software monetisation as a whole for a fast-moving, fully interactive Q&amp;A and platform demo session.</p>
<p>We plan to take over one of these session with a launch party at least once a quarter, so stay tuned for the next one</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Introducing the RevenueCat Codegen Gradle Plugin: type safe entitlements and offerings on Android]]></title>
      <link>https://www.revenuecat.com/blog/engineering/android-codegen</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/android-codegen</guid>
      <pubDate>Thu, 11 Jun 2026 16:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Jaewoong Eum]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[In this article, you'll explore RevenueCat's Codegen Gradle plugin, which generates product data code automatically.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/a89d85ffd5ba9cb817d6601fa43526bd8e1f8baa-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>Every RevenueCat integration shares the same quiet liability: the string keys. Your entitlements, offerings, and packages live in the RevenueCat dashboard, and your Kotlin code reaches for them with raw strings like <code>entitlements[&quot;premium_access&quot;]</code>. The compiler cannot verify those strings, the IDE cannot autocomplete them, and a single typo ships as a runtime bug instead of failing the build. The new <a href="https://github.com/RevenueCat/purchases-android/tree/main/codegen">RevenueCat Codegen Gradle Plugin</a> closes this gap by talking to the RevenueCat API at build time and generating type-safe Kotlin accessors for everything you’ve configured in the dashboard.</p>
<p>In this article, you’ll explore what the plugin generates and why, how its two Gradle tasks fetch and cache your project schema, how to set it up with a version catalog, how naming styles turn dashboard lookup keys into Kotlin identifiers, how caching and offline mode keep builds working without a network, and the trade-offs to weigh before adopting it.</p>
<h2><strong>The fundamental problem: Dashboard keys as raw strings</strong></h2>
<p>Consider the typical code for gating a premium feature and resolving a package to purchase:</p>
<pre><code class="language-kotlin">val isPremium = customerInfo.entitlements[&quot;premium_access&quot;]?.isActive == true
val offering = offerings.getOffering(&quot;perplexity&quot;)
val monthly = offering?.getPackage(&quot;\$rc_monthly&quot;)</code></pre>
<p>This code compiles whether or not <code>&quot;premium_access&quot;</code> exists in your project. If someone renames the entitlement in the dashboard, or a teammate types <code>&quot;premium_acess&quot;</code> in a new screen, nothing fails until a user hits that code path: the entitlement lookup silently returns <code>null</code>, and <code>getPackage</code> throws at runtime. The usual mitigation is a hand maintained constants file, but that file drifts out of sync with the dashboard the moment anyone forgets to update it.</p>
<p>With the codegen plugin applied, the same logic becomes:</p>
<pre><code class="language-kotlin">val isPremium = customerInfo.isPremiumAccessActive
val offering = offerings.perplexity
val monthly = offering?.perplexityMonthly</code></pre>
<p>The difference is more than aesthetics:</p>
<ul>
<li><strong>IDE autocomplete</strong>: every entitlement, offering, and package from your dashboard surfaces as a typed property, so you discover them by typing a dot instead of switching to a browser tab.</li>
<li><strong>Compile time safety</strong>: a typo or a removed entitlement becomes a build error, not a runtime <code>null</code>.</li>
<li><strong>No drift</strong>: the plugin fetches your latest dashboard state at build time, so there is no constants file to maintain by hand.</li>
</ul>
<h2><strong>Setting up the plugin</strong></h2>
<p>The plugin is published to Maven Central as part of the RevenueCat Purchases SDK. The plugin ID is <code>com.revenuecat.purchases.codegen</code>, and its version always matches the Purchases SDK version, so you reuse the version string you already have.</p>
<h3><strong>Step 1: Apply the plugin</strong></h3>
<p>Add it to your version catalog in <code>gradle/libs.versions.toml</code>:</p>
<pre><code class="language-kotlin">[versions]
purchases = &quot;PURCHASES_VERSION&quot;

[plugins]
revenuecat-codegen = { id = &quot;com.revenuecat.purchases.codegen&quot;, version.ref = &quot;purchases&quot; }</code></pre>
<p>Declare it in your root <code>build.gradle.kts</code> so Gradle resolves it for all subprojects:</p>
<pre><code class="language-kotlin">plugins {
    alias(libs.plugins.revenuecat.codegen) apply false
}</code></pre>
<p>Then apply it in your app module’s <code>build.gradle.kts</code>:</p>
<pre><code class="language-kotlin">plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.revenuecat.codegen)
}</code></pre>
<h3><strong>Step 2: Configure the extension</strong></h3>
<p>Only three properties are required: <code>apiKey</code>, <code>projectId</code>, and <code>packageName</code>. Everything else has a default.</p>
<pre><code class="language-kotlin">revenuecat {
    apiKey.set(&quot;sk_your_v2_secret_key&quot;)
    projectId.set(&quot;proj_your_project_id&quot;)
    packageName.set(&quot;com.example.app.rc&quot;)
}</code></pre>
<p><code><strong>apiKey</strong></code> must be a v2 secret key (it starts with <code>sk_</code>) with read permissions, created in the RevenueCat dashboard under Project Settings &gt; API Keys. This key is used only at build time and is never compiled into your app binary. You’ll see how to keep it out of version control in a later section.</p>
<p><code><strong>projectId</strong></code> is your project’s identifier, also found in Project Settings.</p>
<p><code><strong>packageName</strong></code> is the package the generated code lands in, and it must be a package you own, such as <code>com.myapp.rc</code>. Avoid anything under <code>com.revenuecat.*</code>. The Purchases SDK ships a consumer ProGuard rule, <code>-keep class com.revenuecat.** { *; }</code>, which prevents classes in that namespace from being shrunk or obfuscated. If your generated code lands there, R8 retains all of it in release builds even when unused, which bloats your APK for no benefit.</p>
<p>The full set of optional properties looks like this:</p>
<pre><code class="language-kotlin">import com.revenuecat.purchases.codegen.NamingStyle
import com.revenuecat.purchases.codegen.OfflineMode

revenuecat {
    apiKey.set(&quot;sk_your_v2_secret_key&quot;)
    projectId.set(&quot;proj_your_project_id&quot;)
    packageName.set(&quot;com.example.app.rc&quot;)

    cacheTtlMinutes.set(30L)
    offlineMode.set(OfflineMode.USE_CACHE_OR_SKIP)
    namingStyle.set(NamingStyle.CAMEL_CASE)

    generateEntitlements.set(true)
    generateOfferings.set(true)
    generatePackages.set(true)
    generateCustomerInfoExtensions.set(true)
}</code></pre>
<p>The four <code>generate*</code> flags each control one category of output. If you iterate <code>offering.availablePackages</code> dynamically rather than accessing packages by name, for example, set <code>generatePackages.set(false)</code> to keep the generated surface small. The caching and offline options are covered in their own section below.</p>
<h3><strong>Step 3: Build</strong></h3>
<p>Run any standard build task:</p>
<p><code>./gradlew assembleDebug</code></p>
<p>Generation runs automatically before compilation. After the first successful build, do File &gt; Sync Project with Gradle Files in Android Studio so the IDE picks up the new source directory and autocomplete starts working.</p>
<p>If you ever want to fetch or regenerate without a full build, both tasks are invokable directly:</p>
<p><code>./gradlew :app:rcFetchSchema</code>
<code>./gradlew :app:rcGenerateCode</code></p>
<h2><strong>What gets generated</strong></h2>
<p>Suppose your project has a <code>premium_access</code> entitlement and a <code>perplexity</code> offering containing the standard <code>$rc_monthly</code> and <code>$rc_annual</code> packages. The plugin generates four categories of output.</p>
<h3><strong>Entitlement ID constants</strong></h3>
<p>A single <code>RCEntitlementId</code> object holds one constant per entitlement:</p>
<pre><code class="language-kotlin">object RCEntitlementId {
    const val PREMIUM_ACCESS: String = &quot;premium_access&quot;
}</code></pre>
<p>These constants are useful at the boundaries where you still need the raw key, such as logging or analytics events, while keeping the compiler in the loop.</p>
<h3><strong>Type safe </strong><code><strong>EntitlementInfos</strong></code><strong> extensions</strong></h3>
<p>For each entitlement, two extension properties land on <code>EntitlementInfos</code>: an accessor that returns the <code>EntitlementInfo</code> or <code>null</code>, and a boolean shortcut for the common <code>isActive</code> check.</p>
<pre><code class="language-kotlin">val EntitlementInfos.premiumAccess: EntitlementInfo?
    get() = this[&quot;premium_access&quot;]

val EntitlementInfos.isPremiumAccessActive: Boolean
    get() = this[&quot;premium_access&quot;]?.isActive == true</code></pre>
<h3><strong>Convenience </strong><code><strong>CustomerInfo</strong></code><strong> extensions</strong></h3>
<p>The same active check is also generated directly on <code>CustomerInfo</code>, so gating a feature behind a single entitlement skips the <code>entitlements</code> lookup entirely:</p>
<pre><code class="language-kotlin">val CustomerInfo.isPremiumAccessActive: Boolean
    get() = this.entitlements[&quot;premium_access&quot;]?.isActive == true</code></pre>
<h3><strong>Offering and package accessors</strong></h3>
<p>Each offering gets a constant in <code>RCOfferingId</code> and a typed accessor on <code>Offerings</code> that wraps <code>getOffering()</code>:</p>
<pre><code class="language-kotlin">object RCOfferingId {
    const val PERPLEXITY: String = &quot;perplexity&quot;
}

val Offerings.perplexity: Offering?
    get() = this.getOffering(&quot;perplexity&quot;)</code></pre>
<p>Packages need one extra design decision. Multiple offerings commonly share the same package lookup key, since <code>$rc_monthly</code> exists in nearly every offering. Generating a bare <code>monthly</code> extension on <code>Offering</code> would collide the moment a second offering defines the same package. The plugin resolves this by prefixing each package property with its offering name and scoping the ID constants into a per offering object:</p>
<pre><code class="language-kotlin">object RCPerplexityPackageId {
    const val MONTHLY: String = &quot;${'$'}rc_monthly&quot;
    const val ANNUAL: String = &quot;${'$'}rc_annual&quot;
}

val Offering.perplexityMonthly: Package?
    get() = this.getPackage(&quot;${'$'}rc_monthly&quot;)</code></pre>
<p>The <code>${'$'}</code> sequence might catch your eye. Because <code>$</code> begins a string template in Kotlin, KotlinPoet escapes the literal dollar sign in <code>$rc_monthly</code> this way so the generated file always compiles. At runtime the string is exactly <code>$rc_monthly</code>.</p>
<p>Every generated property also carries a KDoc comment with the display name from your dashboard and a reminder that the code reflects the dashboard at build time, so the documentation popup in the IDE tells you what each key actually is.</p>
<h2><strong>Naming styles: From lookup keys to Kotlin identifiers</strong></h2>
<p>Dashboard lookup keys are arbitrary strings, so the plugin runs each one through a pipeline before it becomes an identifier: strip the <code>$rc_</code> prefix, apply the configured naming style, replace characters that are invalid in identifiers, prefix an underscore when the key starts with a digit, and escape Kotlin reserved words with backticks. The result always compiles, even if someone names an entitlement <code>when</code> or <code>2024_promo</code> in the dashboard.</p>
<p>The <code>namingStyle</code> option controls the middle step:</p>
<table>
<thead><tr>
<th><p>Lookup key</p></th>
<th><p><code>CAMEL_CASE</code> (default)</p></th>
<th><p><code>SNAKE_CASE</code></p></th>
<th><p><code>AS_IS</code></p></th>
</tr></thead>
<tbody>
<tr>
<td><p><code>premium_access</code></p></td>
<td><p><code>premiumAccess</code></p></td>
<td><p><code>premium_access</code></p></td>
<td><p><code>premium_access</code></p></td>
</tr>
<tr>
<td><p><code>ProPhoto</code></p></td>
<td><p><code>prophoto</code></p></td>
<td><p><code>pro_photo</code></p></td>
<td><p><code>ProPhoto</code></p></td>
</tr>
<tr>
<td><p><code>$rc_monthly</code></p></td>
<td><p><code>monthly</code></p></td>
<td><p><code>monthly</code></p></td>
<td><p><code>monthly</code></p></td>
</tr>
</tbody>
</table>
<p><code>CAMEL_CASE</code> fits most Kotlin codebases because it follows the standard property naming convention. <code>AS_IS</code> preserves the key exactly after prefix stripping, which is useful when your lookup keys are already well formed identifiers and you want a one to one mapping. The constants inside ID objects like <code>RCEntitlementId</code> are always <code>UPPER_SNAKE_CASE</code> regardless of the style, because constants follow their own convention in Kotlin.</p>
<h2><strong>Caching and offline mode: Builds that don’t depend on the network</strong></h2>
<p>A Gradle plugin that hits the network on every build would be a hard sell, so the fetch step is built around a local cache. <code>rcFetchSchema</code> writes the fetched schema and a timestamp to <code>build/revenuecat/cache/revenuecat-schema.json</code>. On the next build, it checks the file’s age against <code>cacheTtlMinutes</code> (default 30) and skips the network call entirely if the cache is still fresh. Setting the TTL to <code>0</code> forces a fetch on every build.</p>
<p>When a fetch does run and your project has many offerings, the client paces itself: it follows the API’s pagination, waits 500 milliseconds between requests, and retries up to five times with exponential backoff when it hits a rate limit response. This makes the first build slower on large projects, but every subsequent build reads from the cache.</p>
<p>The interesting question is what happens when the fetch fails: no network on a flight, a connection timeout, or a key that was revoked. The <code>offlineMode</code> option offers two behaviors:</p>
<ul>
<li><code><strong>USE_CACHE_OR_SKIP</strong></code> (default): if a stale cache exists, it is used and a warning logs the cache age. If no cache exists at all, generation is skipped with a log message and the build continues without generated sources. Local builds keep working with no intervention.</li>
<li><code><strong>FAIL</strong></code>: the build fails immediately with a <code>GradleException</code> describing the error. This is the right choice for a release pipeline where you need a hard guarantee that the generated code reflects the latest dashboard state.</li>
</ul>
<p>A practical pattern applies each mode based on the environment:</p>
<pre><code class="language-kotlin">import com.revenuecat.purchases.codegen.OfflineMode

val isCI = providers.environmentVariable(&quot;CI&quot;).isPresent

revenuecat {
    apiKey.set(providers.environmentVariable(&quot;REVENUECAT_API_KEY&quot;).getOrElse(&quot;&quot;))
    projectId.set(providers.environmentVariable(&quot;REVENUECAT_PROJECT_ID&quot;).getOrElse(&quot;&quot;))
    packageName.set(&quot;com.example.app.rc&quot;)
    offlineMode.set(if (isCI) OfflineMode.FAIL else OfflineMode.USE_CACHE_OR_SKIP)
}</code></pre>
<p>With this setup, a local build falls back to the cache when the network is unavailable, while CI refuses to ship a build whose generated code might be stale.</p>
<p>Note that the fallback applies to fetch failures, not to missing configuration. An empty <code>apiKey</code> fails the build immediately with “revenuecat.apiKey must be configured in your build script.” regardless of the offline mode, so every developer needs the key configured even when a cache is present.</p>
<p>One more consequence of <code>USE_CACHE_OR_SKIP</code> is worth calling out: if your very first fetch fails, generation is skipped and references to the generated code fail to compile with unresolved symbols. If you hit “No RevenueCat schema cache found. Skipping code generation.” in the build log, temporarily set <code>offlineMode.set(OfflineMode.FAIL)</code> to surface the underlying error, which is usually a wrong <code>apiKey</code> or <code>projectId</code>.</p>
<h3><strong>Refreshing after dashboard changes</strong></h3>
<p>The generated code reflects your dashboard at the time of the last fetch. After you add or rename an entitlement, a normal build picks the change up once the TTL expires. To pick it up immediately, wipe the cache and rerun the tasks:</p>
<pre><code class="language-kotlin">rm -rf app\/build\/revenuecat\/cache
.\/gradlew :app:rcFetchSchema :app:rcGenerateCode</code></pre>
<p>Then sync the project in Android Studio so the IDE reindexes the regenerated sources.</p>
<h2><strong>How it works: Two Gradle tasks and a schema cache</strong></h2>
<p>The plugin registers a <code>revenuecat { }</code> extension and two Gradle tasks under the <code>revenuecat</code> group:</p>
<ol>
<li><code><strong>rcFetchSchema</strong></code> calls the RevenueCat API v2 (<code>/projects/{projectId}/entitlements</code>, <code>/projects/{projectId}/offerings</code>, and <code>/projects/{projectId}/offerings/{offeringId}/packages</code>), follows pagination, and writes the result to a JSON cache at <code>build/revenuecat/cache/revenuecat-schema.json</code> along with a timestamp.</li>
<li><code><strong>rcGenerateCode</strong></code> reads that cache and generates Kotlin source files into <code>build/generated/revenuecat/kotlin/</code> using KSP.</li>
</ol>
<p>You never invoke either task by hand during normal development. The plugin wires <code>rcGenerateCode</code> into your compile tasks, so any standard build runs generation first.</p>
<h2><strong>Keeping the secret key out of version control</strong></h2>
<p>The <code>apiKey</code> is a v2 secret key, so even though it never reaches your app binary, it should not be committed in <code>build.gradle.kts</code>. The generated files contain only plain lookup key strings like <code>&quot;premium_access&quot;</code>, never the key itself, but the build script is checked in. For local development, read the key from <code>local.properties</code>, which stays out of version control:</p>
<pre><code class="language-kotlin">val localProps = java.util.Properties().apply {
    rootProject.file(&quot;local.properties&quot;).takeIf { it.exists() }?.inputStream()?.use { load(it) }
}

revenuecat {
    apiKey.set(localProps.getProperty(&quot;REVENUECAT_API_KEY&quot;, &quot;&quot;))
    projectId.set(localProps.getProperty(&quot;REVENUECAT_PROJECT_ID&quot;, &quot;&quot;))
    packageName.set(&quot;com.example.app.rc&quot;)
}</code></pre>
<p>Then add the values to <code>local.properties</code>:</p>
<pre><code class="language-text">REVENUECAT_API_KEY=sk_your_v2_secret_key
REVENUECAT_PROJECT_ID=proj_your_project_id
</code></pre>
<p>For CI, inject both values as environment variables, as shown in the offline mode example above.</p>
<h2><strong>Trade offs: When generated accessors fit and when they don’t</strong></h2>
<p>The plugin fits best with stable identifiers. Entitlement IDs rarely change once set, and turning every entitlement check into a compile time verified property removes a whole class of bugs. Offerings and packages benefit the same way when their IDs are stable, especially in a large codebase where autocomplete and rename refactoring matter.</p>
<p>For highly dynamic offerings, keep one trade off in mind. The generated code is a snapshot of your dashboard at build time. Adding a new package or renaming an offering in the dashboard requires a new build and a new app release before the generated accessors reflect it. If you iterate <code>offering.availablePackages</code> at runtime instead, an already shipped app picks up new packages without a release, because the package list comes from the RevenueCat backend at runtime.</p>
<p>In practice this matters less than it sounds. Adding a new package that you intend to reference by name in code requires writing new code anyway, which means a new release regardless. But if you drive your paywall presentation entirely from the dashboard without touching code, the runtime API keeps that flexibility, and you can disable <code>generatePackages</code> while keeping the entitlement and offering accessors. The two approaches compose: use the generated properties where identifiers are stable, and the dynamic API where the dashboard is the source of truth.</p>
<h2><strong>Conclusion</strong></h2>
<p>In this article, you’ve explored the <a href="https://github.com/RevenueCat/purchases-android/tree/main/codegen">RevenueCat Codegen Gradle Plugin:</a> the raw string problem it removes, the <code>rcFetchSchema</code> and <code>rcGenerateCode</code> tasks that fetch your dashboard schema and generate typed Kotlin accessors, the setup from version catalog to first build, the naming pipeline that turns lookup keys into identifiers that always compile, and the caching and offline modes that keep builds fast and network independent. If your entitlement checks are still string lookups, applying the plugin turns the next typo into a build error instead of a support ticket.</p>
<p>As always, happy coding!</p>
<p>— <a href="https://github.com/skydoves/">Jaewoong</a> (skydoves)</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to build a reactivation strategy that actually works]]></title>
      <link>https://www.revenuecat.com/blog/growth/app-reactivation-strategy-how-to</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/growth/app-reactivation-strategy-how-to</guid>
      <pubDate>Thu, 11 Jun 2026 14:00:00 GMT</pubDate>
      <dc:creator><![CDATA[Alice Muir Kocourková]]></dc:creator>
      <category><![CDATA[Growth]]></category>
      <description><![CDATA[Most apps treat cancellation as the end of the relationship. It's actually step zero of reactivation.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/f050222e4c88f1ba36e4cd8f0eca716cb0cc746f-1600x850.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>A lot of subscription apps either make the mistake of treating churn as the end point and never bother to follow up with these users or they spam them with discounts, believing this is the only strategy that works to bring them back.</p>
<p>Churned users are actually one of the most valuable segments you have. They’ve already crossed the hardest threshold: they trusted you enough to pay. And yet, most apps either ignore them completely or hit them with a random, out-of-context, discount three months later. Reactivation is a whole strategy in itself, not just a campaign. If you build it properly, it becomes one of the highest ROI levers in your entire growth model.</p>
<p>According to RevenueCat’s <a href="https://www.revenuecat.com/state-of-subscription-apps/">State of Subscription Apps 2026 Report</a>:</p>
<ul>
<li>Monthly subscribers come back at ~18–24% within a year</li>
<li>Even weekly plans see ~7–10% reactivation</li>
<li>Only annual plans behave like “true churn” (~4–6%)</li>
</ul>
<p>So let’s look at some of the ways you can build a strategy around reactivation.</p>
<h2><strong>1. Start at the moment of churn: Your cancellation flow is step zero</strong></h2>
<p>Many teams treat cancellation as the last touchpoint with a user (e.g. using “Sorry to see you go”-style messaging). However, cancellation is a great opportunity for data collection segmentation and follow-up. This is where your reactivation strategy actually begins.</p>
<h3><strong>What to implement</strong></h3>
<p>A simple questionnaire that triggers on user cancellation and asks them why they no longer wish to use or pay for the app or product.</p>
<h3><strong>What to ask</strong></h3>
<p>Keep it straightforward – One clear question should suffice.</p>
<p>Not only are you looking to collect valuable insights here, you’re also tagging intent so you can follow up with users later with messaging that’s appropriate for them specifically.</p>
<p>Focus on actionable reasons like:</p>
<ul>
<li>“I don’t need it right now”</li>
<li>“Too expensive”</li>
<li>“Missing features I need”</li>
<li>“Technical issues”</li>
<li>“I didn’t get value”</li>
</ul>
<p>Each of these should map directly to a future reactivation path.</p>
<p>Note that in every app I’ve worked with, there will be a portion of users who think the product is too expensive or simply don’t want to pay. Unless your conversion is below <a href="https://www.revenuecat.com/healthscore/">benchmarks</a>, I’d take these responses with a grain of salt.</p>
<p>Make sure to also leave an “other” option open-ended so you’re collecting insights from users who may have discovered a bug or problem with the app you’re not aware of.

Once you’ve got a breakdown of the most popular answers, you can define your list of actionable reasons further.</p>
<h3><strong>What to offer</strong></h3>
<p>This is where most apps throw discounts immediately. That trains users to churn to get a better deal, and it also cheapens the value perception of your product (e.g. why did I pay $39.99 when I could have got this for $20?)</p>
<p>Instead, take each individual cancellation reason from your survey and map out a personalized reactivation plan.</p>
<p>Some examples:</p>
<ul>
<li>“Not needed right now” → Offer <strong>pause</strong> instead of cancel</li>
<li>“Too expensive” → Offer <strong>lower tier or annual reframing</strong></li>
<li>“Didn’t get value” → Offer <strong>guided setup or feature education</strong></li>
<li>“Technical issues” → Offer <strong>support escalation</strong></li>
</ul>
<p>Not every churn needs to be saved. Some should be <em>cleanly segmented for later</em>.</p>
<h3><strong>What data to capture</strong></h3>
<p>For each churned user, you want to capture the following data points:</p>
<ul>
<li>Churn reason</li>
<li>Plan type (monthly vs annual)</li>
<li>How long they were a user</li>
<li>Usage intensity before churn</li>
<li>Last meaningful action</li>
</ul>
<p>If you’re not capturing these data points, your reactivation strategy will be generic and impersonal. For example, you won’t know if this was a power user who just dropped out of the habit or a low frequency user who may need time before the use case is relevant again.</p>
<p>On iOS, there’s also a platform-level lever here that’s still relatively new and underused: <a href="https://www.revenuecat.com/blog/engineering/apple-retention-messaging-api/">Apple Retention Messaging API.</a></p>
<p>This allows developers to present a targeted message or offer directly within the App Store cancellation flow, before the subscription is fully canceled. In other words, you get one last, native touchpoint at peak intent.</p>
<p>Access and adoption are still evolving, so not every team is using it yet. But it’s worth keeping on your radar, because it fundamentally changes what’s possible at the moment of churn.</p>
<p>Used well, this can reinforce the same logic as your in-app cancellation flow:</p>
<ul>
<li>Surface a pause or downgrade option instead of pushing discounts</li>
<li>Address common churn reasons with a clear value reminder</li>
</ul>
<p>Used badly, it becomes another place to throw generic discounts and train users to game your system.</p>
<h2><strong>2. The post-churn relationship: how to stay present without being annoying</strong></h2>
<p>You want to stay relevant enough that returning feels natural, not forced.Instead of generic “come back” emails, try to focus on value reminders. Remember your product is still there to solve a user’s problem – so messaging should be centred around that, and this is where your data collection from Step 1 comes in handy.</p>
<ul>
<li>“Here’s what’s new since you left” <strong>– could target the “didn’t get value” or “technical issues” segments.</strong></li>
<li>“People like you are using X feature to achieve Y” <strong>– could target the “didn’t get value” segment.</strong></li>
<li>“Quick win you can get in 2 minutes” <strong>– could target the “didn’t get value” or “not needed right now” segments.</strong></li>
</ul>
<p>Think of it less like win-back and more like lightweight re-education. You’re rebuilding perceived value over time and just like all lifecycle messaging, the strategy needs to align exactly with the user’s goals.</p>
<h3><strong>Channel strategy</strong></h3>
<p>Email alone likely won’t carry your reactivation strategy. The strongest setups layer channels so the message shows up in the right place at the right time. Email should still do most of the heavy lifting, but it works best when reinforced with push for users who are still opted in, and paid retargeting for higher LTV churned users where the economics justify it.</p>
<p>The trap is thinking more channels means more pressure. It doesn’t. Frequency and coordination matter far more than channel count. If a user gets the same “come back” message three times in two days across different touchpoints, it feels like you’re chasing them.</p>
<p>A better approach would be along the lines of one well-timed email a few days after churn, a contextual push a week later, and a lightweight retargeting touchpoint only if the user is high value. Different messages, spaced out, and each with a clear purpose.</p>
<h2><strong>3. Timing matters more than what you say</strong></h2>
<p>Timing is where most reactivation strategies fall apart. Sending the same message to everyone 30 days after churn is easy to set up, but it ignores the fact that not all churn behaves the same. The biggest difference comes down to plan type.</p>
<p>Monthly and annual subscribers churn for very different reasons, and more importantly, they forget you at different speeds.</p>
<p>For monthly users, the relationship is short and transactional. Habits break quickly, and if you wait too long, you’re no longer reactivating, you’re reacquiring. That’s why it’s worth testing earlier touchpoints:</p>
<ul>
<li>Around <strong>7 days</strong>: while the habit is still fresh</li>
<li>Around <strong>21 days</strong>: before they fully disengage</li>
<li>Around <strong>45 days</strong>: a final attempt before they go cold</li>
</ul>
<p>For <strong>annual users</strong>, the dynamic is different. They’ve made a bigger commitment and usually had stronger initial intent. Churn is often driven by timing or changing needs, not lack of belief in the product. That gives you a longer window to re-engage:</p>
<ul>
<li>Around <strong>30 days</strong>: once they’ve had some distance</li>
<li>Around <strong>90 days</strong>: when the original use case might return</li>
<li>Around <strong>6 months</strong>: aligned with seasonal or cyclical needs</li>
</ul>
<p>The key idea is that reactivation works best when it matches the user’s <a href="https://phiture.com/mobilegrowthstack/natural-usage-habits-choosing-your-engagement-metric-a158438962c3/">natural usage habit</a>, not your CRM calendar.</p>
<p>Plan type is only one part of the picture. To make timing actually work, you also need to layer in why the user churned and how they behaved before leaving.</p>
<p>Start with their churnreason. Different reasons create very different reactivation windows:</p>
<ul>
<li><strong>“Not needed right now”</strong>
This isn’t a rejection, it’s a timing issue. Reaching out too early feels pushy because the need genuinely isn’t there. You’re better off waiting until the use case naturally returns, whether that’s tied to travel, routines, or specific moments.</li>
<li><strong>“Too expensive”</strong>
Price sensitivity doesn’t disappear overnight. Instead of immediately offering discounts, time your outreach around moments where value feels higher, like seasonal spikes, new feature releases, or more relevant use cases.</li>
<li><strong>“Didn’t get value”</strong>
This is the one case where speed matters. The longer you wait, the more the product is mentally written off. Re-engage quickly with education, guided use cases, or a clearer path to first value.</li>
<li><strong>“Technical issues”</strong>
Timing here is simple: don’t reach out until the problem is fixed. Coming back too early just reminds the user why they left.</li>
</ul>
<p>Then you can layer in behavior. Not all churned users are equal:</p>
<ul>
<li><strong>Highly engaged users who churned</strong>
Their habit just broke. Your window is short, because they’re more likely to replace you quickly. Early, relevant touchpoints matter here.</li>
<li><strong>Low-engagement users who churned</strong>
They never really built a habit in the first place. At this point, you’re not reactivating, you’re essentially reacquiring. That means slower timing and more emphasis on value explanation.</li>
</ul>
<p>
In summary, message timing shouldn’t just reflect when someone left, but why they left and how close they were to forming a habit in the first place.</p>
<h2><strong>4. Offer strategy: what works vs what feels desperate</strong></h2>
<p>Let’s be blunt. If your only reactivation lever is “20% off”, your product isn’t doing enough heavy lifting. Discounts only work if they feel timely and relevant to the user.</p>
<p>The better approach is to make your offer feel relevant, not reactive. That starts with contextual offers.</p>
<p><strong>1. Instead of a generic incentive, tie the message directly to why the user left:</strong>
“You canceled because X. Here’s a solution for that.”
This immediately makes the outreach feel personal.</p>
<p><strong>2. Then look at usage-based incentives.</strong>
Credits, limited access, or feature unlocks reduce the barrier to re-entry without forcing a full commitment. They let users ease back into the product instead of making an all-or-nothing decision.</p>
<p><strong>3. Time-based framing is another underrated lever.</strong>
For users who churned due to timing, messages like “Try again for your next trip” or “Restart when you need it” align with real-world behavior. You’re not pushing them to come back now, you’re giving them a reason to come back when it actually makes sense.</p>
<p><strong>4. Lean on product-led hooks.</strong>
New features, meaningful improvements, or integrations can be powerful reactivation drivers, especially for users who left due to missing value. This is where your product does the convincing for you.</p>
<p>Remember, the goal isn’t to win everyone back. It’s to win back the right users (high value, high intent), for the right reasons, in a way that actually sticks.</p>
<h2><strong>5. Make it frictionless to return</strong></h2>
<p>Even if your messaging is perfect, unnecessary friction can kill your reactivation efforts.</p>
<p>A lot of teams get the user to click and then lose them the moment they land back in the app because the experience feels like starting from scratch. If a returning user is treated like a brand new one, forced through generic onboarding, or dropped into an impersonal state, you’ve just undone all the work it took to bring them back. Instead, make the return feel like a continuation, not a reset.</p>
<p>Start by removing unnecessary friction. If you can avoid re-onboarding, do it. Let users pick up where they left off, with their preferences, history, and progress intact. The more familiar the experience feels, the faster they’ll get back to value.</p>
<p>Then reduce decision fatigue. Don’t make users rethink everything from scratch. Default to their previous plan, clearly highlight what’s changed since they left, and guide them toward an immediate “quick win” so they can feel progress right away.</p>
<p>Finally, consider more flexible ways to re-enter. Options like pausing instead of canceling, easy plan switching, or grace periods all lower the psychological barrier to coming back. You’re not asking for a big commitment, you’re offering a low-risk way to try again.</p>
<h2><strong>6. Ultimately, the quality of your product determines reactivation success</strong></h2>
<p>Whether your reactivation strategy works is directly tied to everything that happens before a user churns, especially your onboarding quality, activation success, value delivery, and pricing and packaging.</p>
<p>In other words, if users churn because they didn’t understand the product, didn’t experience value, or barely used it, no amount of win-back messaging will fix that.</p>
<p>I go deeper into lifecycle architecture in <a href="https://www.startapp.school/courses/lifecycle-marketing-for-apps">my course</a> on RevenueCat StartApp School. Reactivation only works when the system before it is solid.</p>
<h2><strong>Final thought</strong></h2>
<p>It’s easy to assume churned users are no longer interested in your product and might be a waste of time to pursue. However, the data shows that this is where some of the highest-leverage growth sits.</p>
<p>You already paid to acquire these users and convinced them to pay. Are you building a system where you’re top of mind when they’re ready to come back? And does that return experience help them pick up where they left off?</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[WWDC26: What’s new for subscription apps]]></title>
      <link>https://www.revenuecat.com/blog/engineering/wwdc26-whats-new-for-apps</link>
      <guid isPermaLink="false">https://www.revenuecat.com/blog/engineering/wwdc26-whats-new-for-apps</guid>
      <pubDate>Tue, 09 Jun 2026 18:01:53 GMT</pubDate>
      <dc:creator><![CDATA[Austin Blake]]></dc:creator>
      <category><![CDATA[Engineering]]></category>
      <description><![CDATA[Monthly-billed annual plans, save offers at cancellation, and team sales – the StoreKit and App Store changes that actually move your business.]]></description>
      <enclosure url="https://cdn.sanity.io/images/c3qnx9b0/production/b5c9b1b635baf2aa836662d3805ba044824c0877-1600x800.png" length="0" type="image/*"/>
      <content:encoded><![CDATA[<p>WWDC26 has wrapped up and there’s a lot to dig into for subscription app businesses. We’ve been going through the session videos and release notes to figure out what Apple has changed this year that actually matters for developers building with RevenueCat.</p>
<p>Most of the keynote was focused on Siri AI and iOS 27’s design updates, but the StoreKit and App Store sessions had some of the most significant subscription business changes in years. Here’s everything worth knowing.</p>
<h2><strong>Annual subscriptions can now bill monthly</strong></h2>
<p>(By the way, this is only available outside of the United States and Singapore for now, and requires iOS/iPadOS/macOS/tvOS/visionOS 26.5 or later.)</p>
<p>Apple has introduced a new pricing option that lets customers pay for an annual subscription in monthly installments. The customer still commits to a full year, but the cost is spread across twelve monthly payments instead of charged upfront.</p>
<p>This is a meaningful new tool for paywalls. Annual plans convert well because of the price-per-month math, but the upfront cost is a real barrier for a lot of users. Monthly-billed annual plans give you the retention benefits of an annual commitment without asking users to hand over a year’s worth of money at once.</p>
<p>You can add this billing option to new or existing one-year auto-renewable subscriptions in App Store Connect.</p>
<h3><strong>What changes in your code</strong></h3>
<p>The entry point on the client side is a new pricingTerms property on a product’s subscription info. It returns an array of every billing plan available for that product. Every annual subscription has at least one entry with a <code>billingPlanType</code> of <code>.upFront</code>. If you’ve configured a commitment plan, a second entry shows up with a <code>billingPlanType</code> of <code>.monthly</code>.</p>
<p>There’s a new <code>preferredSubscriptionPricingTerms</code> view modifier for <code>SubscriptionStoreView</code> if you’re using StoreKit’s built-in UI. For custom paywall UI, read <code>pricingTerms</code> directly to get both the monthly price and the total 12-month commitment price so you can display both clearly to users.</p>
<p>To trigger the purchase, pass the billing plan as a purchase option:</p>
<pre><code class="language-text">let result = try? await product?.purchase(options: [.billingPlanType(.monthly)])</code></pre>
<h3><strong>What changes on your server</strong></h3>
<p>The JWS transaction payload has new fields to be aware of: <code>billingPlanType</code>, <code>renewalBillingPlanType</code>, and <code>commitmentInfo</code>. The <code>commitmentInfo</code> block tells you which billing period a customer is in, the total number of billing periods, the committed price, and the commitment expiration date.</p>
<p>This is important: a single annual product ID can now represent two completely different billing arrangements. Make sure your backend is reading and storing these new fields, or you’ll end up with incorrect entitlement data.</p>
<h3><strong>What this means for RevenueCat customers</strong></h3>
<p>Your paywalls will need to show both the monthly price and the total 12-month commitment. “Pay $4.99/month, billed over 12 months” is not the same thing as “$59.99/year” and your users will expect to see both.</p>
<p>In analytics, committed monthly billing is not the same as a regular monthly subscription. If you don’t segment them, your LTV models will be wrong.</p>
<p>One testing caveat: there’s a known Xcode bug where <code>pricingTerms.commitmentInfo.price</code> returns an incorrect price for commitment plans in StoreKit Testing. Keep that in mind as you build and test.</p>
<p>RevenueCat supports committed monthly billing out of the box. We parse and store the new <code>billingPlanType</code>, <code>renewalBillingPlanType</code>, and <code>commitmentInfo</code> fields automatically, so you don’t need to build custom backend logic to handle them. See the <a href="https://www.revenuecat.com/docs/subscription-guidance/apple-monthly-with-commitment">docs</a> for implementation details.</p>
<h2><strong>Retention Messaging lets you save subscribers at the moment they cancel</strong></h2>
<p>Retention Messaging is one of the most interesting business tools Apple has shipped in a while. When a subscriber goes to cancel, the App Store can now show them a message or offer that you’ve configured. This is access to a moment in the subscription lifecycle that developers previously had no visibility into.</p>
<p>There are two ways to use it.</p>
<p>The App Store Connect path requires no custom server infrastructure — you configure messages and offers directly in App Store Connect and Apple handles the rest. The real-time path is more powerful but optional.</p>
<p><strong>App Store Connect configuration</strong> is the simpler path. You set up messages in App Store Connect, optionally attach a retention offer and creative assets from the new Asset Library, and Apple displays them during the cancellation flow. No additional server infrastructure required.</p>
<p><strong>Real-time Retention Messaging</strong> is more powerful. When a user tries to cancel, the App Store makes a server-to-server request to an endpoint you configure, passing fields like <code>originalTransactionId</code>, <code>productId</code>, and <code>userLocale</code>. Your server responds with what to show: a message, an alternate product, or a promotional offer.</p>
<p>This opens up subscriber-aware save flows that weren’t possible before. You can offer a discounted annual plan to a loyal monthly subscriber, suggest a lower tier to someone on a premium plan, skip the offer entirely for someone who already redeemed one recently, or show different messaging by market.</p>
<p>Apple shared early data in the WWDC session (session 309): subscriptions using Retention Messaging saw an average save-rate lift of 1.4 percentage points, equivalent to an 82% increase. Promotional offer messages had the strongest lift at 5.5 percentage points. Apple noted that results vary by developer.</p>
<p>One important technical requirement: real-time Retention Messaging requires a fast server response. Apple runs a sandbox performance test and your endpoint has to pass before you can use real-time messaging in production. If your server doesn’t respond in time, the App Store falls back to your App Store Connect configuration. Plan for that fallback path.</p>
<p>Real-time retention also works with the new commitment billing plans. You can respond to a cancellation request by offering a switch to the monthly-billed annual plan, which is a useful save option for users who are cancelling because of the upfront cost.</p>
<h3><strong>What this means for RevenueCat customers</strong></h3>
<p>Retention offer transactions have their own identifiers in the JWS payload: <code>offerType</code>: <code>5</code>, <code>offerIdentifier</code>, and <code>offerDiscountType</code>. Make sure your analytics pipeline tracks these separately from other promotional offers so you can measure actual save rates, post-save retention, and which offer types are performing.</p>
<p>RevenueCat handles the real-time Retention Messaging server side for you. The dashboard lets you configure all four message types — text, text + image, switch plan, and promotional offer — and manages the response window requirement automatically. You’ll still need to complete Apple’s approval form to register the endpoint, but the infrastructure is taken care of. Full setup steps are in the <a href="https://www.revenuecat.com/docs/platform-resources/apple-platform-resources/apple-retention-messaging-api">docs</a>.</p>
<h2><strong>Subscriptions can now be sold to groups and organizations</strong></h2>
<p>Apple has introduced two new ways to sell subscriptions beyond the individual consumer model.</p>
<p><strong>Group purchases</strong> let a single subscriber buy multiple seats and invite others to join from inside your app. Apple handles the invitation flow; each person joins using their own Apple Account. This is available for in-app purchases you build yourself with the StoreKit 2 purchase flow.</p>
<p><strong>Volume purchasing</strong> puts your subscription in front of enterprise and education buyers through Apple Business Manager and Apple School Manager. Seat assignments are handled through existing device management workflows, which means IT can deploy your app to a fleet of devices without users needing to do anything themselves.</p>
<p>Volume purchasing is available this fall. Group purchases arrive this winter. Both are configured in App Store Connect and are on by default for most auto-renewable subscriptions (Family Sharing subscriptions are opted out).</p>
<p>Volume pricing bands are configurable: you can set up to five price tiers with reduced per-seat pricing for larger purchases.</p>
<p>On the StoreKit side, Apple added <code>Transaction.OwnershipType.assigned</code> and <code>Transaction.RevocationType.assignmentRevoked</code> enum values to support volume purchases. Transaction query methods also now return transactions assigned to a Managed Apple Account.</p>
<h3><strong>Entitlement design is the hard part</strong></h3>
<p>If your app has any team, education, or organization use case, think through these before you enable group subscriptions:</p>
<ul>
<li>The buyer is often not the end user</li>
<li>One transaction can produce multiple entitled seats</li>
<li>Seats can be assigned, revoked, or reassigned</li>
<li>Volume purchases may arrive through device management rather than the app itself</li>
<li>Volume pricing changes your per-seat revenue metrics</li>
</ul>
<p>This is genuinely new territory for most subscription apps. The purchasing layer is simpler now, but the entitlement logic is more complex.</p>
<p>One implementation detail worth flagging now: both group purchases and volume purchasing require StoreKit 2. If your app hasn’t migrated yet, it’s worth prioritizing that before these features become available later this year to avoid delays.</p>
<p>We’ll have updates later this year on how to implement this in RevenueCat.</p>
<h2><strong>Cross-developer Bundles and Suites</strong></h2>
<p>This is the App Store change getting the most attention in the broader developer community, and it’s worth understanding what it actually is.</p>
<p><strong>App Store Bundles</strong> let developers from different companies package their subscriptions together and offer them at a combined discount. Users can still buy each subscription individually; the bundle is an additional offering alongside the standalone products.</p>
<p><strong>App Store Suites</strong> are different: a collection of subscriptions that only exist as a package and can’t be purchased separately. They’re designed for tightly related apps from different developers that make more sense together than apart.</p>
<p>Both formats address something developers have been asking for for years: cross-developer bundling without requiring the same parent company.</p>
<p>The StoreKit API for Bundles and Suites is available to prototype in Xcode 27 today. New <code>Product.ProductType</code> values represent Bundles and Suites, and <code>Product.SubscriptionInfo.BundledSubscription</code> lets you fetch merchandising data about subscriptions in a bundle. <code>Transaction</code> and <code>RenewalInfo</code> also have new fields for Bundle and Suite status.</p>
<p>That said, Apple has said full program details are coming later in 2026. Prototype against the API now, but don’t plan a ship date yet.</p>
<h3><strong>What this means for your RevenueCat Dashboard</strong></h3>
<p>Bundle and Suite subscribers will look different in your analytics from direct subscribers. Revenue attribution, churn analysis, and per-app subscriber counts will all need to account for the bundle layer. We’ll have more to share on RevenueCat support as the program details become clearer.</p>
<h2><strong>App Store Merchandising: new placements for Creative Assets</strong></h2>
<p>Apple is adding new creative placements on the App Store this year. Developers can now supply rich images and videos that appear in product page headers and search results, and these assets also work with custom product pages and product page optimization tests. They’re managed through a new Asset Library in App Store Connect.</p>
<p>A few things worth noting about how this works:</p>
<ul>
<li>Assets can be submitted for App Review independently from an app update, so you can refresh seasonal creative or coordinate with an Apple Ads campaign without shipping a new build</li>
<li>The Asset Library centralizes all your creative across custom product pages, In-App Events, and now these new placements, so you’re not uploading the same assets in multiple places</li>
<li>A new product page preview lets you see exactly how your page looks across languages, Dark Mode, and device orientations before you publish</li>
</ul>
<p>The same Asset Library also feeds Retention Messaging. That means your creative team can manage App Store acquisition assets and cancellation-save assets from the same place. That’s a meaningful workflow improvement for teams that have historically treated those as completely separate functions.</p>
<h2><strong>Personalized Collections and App Notes</strong></h2>
<p>Apple is rolling out two new discovery surfaces on the App Store. Personalized Collections are curated lists that appear on the Apps, Games, and Search tabs, surfacing apps based on each user’s interests and behavior. App Notes are short explanations that appear alongside a recommendation, telling users why a specific app is being suggested to them.</p>
<p>Both are already rolling out in English (US), with more languages and regions coming later this year.</p>
<p>There’s no developer action required to participate. That said, apps with complete, accurate metadata — clear descriptions, relevant keywords, up-to-date screenshots — will be better positioned to surface in relevant collections. It’s a good time to audit your App Store listing if you haven’t recently.</p>
<h2><strong>Offer Code Redemption gets a proper result</strong></h2>
<p>A smaller but welcome StoreKit update: the offer code redemption sheet now returns a <code>VerificationResult</code> when redemption completes, rather than firing and forgetting.</p>
<p>On success, you get a <code>VerificationResult</code> containing a Transaction to verify and finish. On failure, you get a descriptive error. The API also now takes a set of <code>RedeemOption</code> values to configure the flow.</p>
<p>This brings offer code redemption in line with how the rest of StoreKit 2 works. If you have a custom “redeem code” button in your app, it’s worth updating the transaction handling path to use the new result.</p>
<h2><strong>Unified App Review submissions</strong></h2>
<p>A useful operational change: you can now group In-App Purchase products into a single App Review submission alongside other item types like in-app events, custom product pages, and product page optimizations.</p>
<p>Previously you had to submit each product individually and track them separately. Now you select everything together, submit once, and track review status from one view. The App Store Connect API’s <code>reviewSubmissions</code> collection now supports IAP, subscription, and subscription group resources. Apple is deprecating the older per-resource submission endpoints in favor of this unified path.</p>
<p>If you have tooling or automation built around the old individual submission flow, start migrating now rather than waiting.</p>
<h2><strong>App Review Guidelines: saturated categories now face removal, not just rejection</strong></h2>
<p>Alongside the new monetization features, Apple updated its App Review Guidelines in a way that’s worth knowing about.</p>
<p>Previously, guideline 4.3(b) warned developers not to submit new apps in categories that were already crowded with low-effort entries. Apple would reject new submissions in categories like flashlight apps, fortune telling apps, and dating apps unless they offered something meaningfully different.</p>
<p>The updated guidelines go further. Apple now says it may remove existing apps in well-established saturated categories if they are not “updated, improved, or attracting customers.” The list of targeted categories has also been expanded to include wallpaper apps, simple timers, and sound effects. Apple also added language calling repeated submissions of low-effort apps “mediocre” and “low-quality,” with a warning that developers who keep submitting them may lose Apple Developer Program access entirely.</p>
<p>This is a meaningful shift from “we won’t accept new ones” to “we may remove the ones already there.”</p>
<p>It connects directly to Apple’s new Personalized Collections discovery feature. Apple is building better ways for users to find quality apps, and low-quality clutter degrades those surfaces. The two moves go together.</p>
<p>For existing developers building legitimate subscription businesses, this is more tailwind than headwind. A cleaner App Store, optimized for apps that are actively maintained, is a welcome addition!</p>
<h2><strong>Summary</strong></h2>
<p>WWDC26 brought more than just the usual updates—it was a really exciting year for subscription apps! Apple has introduced some fantastic new ways to help your business grow and connect with users. Here’s a quick look at the highlights:</p>
<ul>
<li><strong>Committed annual billing</strong> gives you a new middle-ground pricing option that could convert users who balk at the annual upfront cost</li>
<li><strong>Retention Messaging</strong> gives you a save opportunity at the moment of cancellation that didn’t exist before</li>
<li><strong>Group purchases and volume purchasing</strong> open up B2B and team sales paths without building your own billing infrastructure</li>
<li><strong>Cross-developer Bundles and Suites</strong> are coming later this year, with an API available now to prototype</li>
<li><strong>Creative Assets</strong> expand your merchandising surface on the App Store</li>
<li><strong>Updated App Review Guidelines</strong> raise the quality floor across the store</li>
</ul>
<p>We know there’s a lot to dig into here, especially regarding analytics and how you design your entitlements. We’re already hard at work figuring out the best ways for RevenueCat to support you through these changes, and we’ll be sharing more updates as we learn more. We’re excited to see what you build!</p>]]></content:encoded>
    </item>
  </channel>
</rss>