Build Revenue-Generating Apps with Antigravity — From Stripe Integration to Store Launch & Monthly Revenue
Implement Stripe billing in your Antigravity app and monetize on the App Store and Google Play. Subscription design, IAP, webhook idempotency, and unifying Stripe + IAP state — written from real indie-dev operations.
When I started building apps as a solo developer back around 2014, my monetization was almost entirely ads. I dropped AdMob into wallpaper apps and small relaxation apps and watched impressions and click-through rates. It worked, in its way. But at some point I wanted to rebuild it so that people paid, directly, for the features they actually wanted. Ads get more intrusive the more someone uses your app; paid features get more valuable the more they are used. That asymmetry, I came to feel, matters a great deal for an app you intend to keep alive for years.
Since I started assembling apps with Antigravity, that kind of rebuild has become much faster. Billing is a heavy area — backend, client, and store review all tangle together, and it is a lot for one person to hold at once. Here I will walk through a Stripe-centered billing implementation, including the places where I personally stumbled while running the membership for Dolice Labs, and I will leave the design reasoning attached to each step. The code is runnable, and I always say why it is written the way it is.
Choosing a Monetization Model: Subscriptions, IAP, and Ads
Apps built with Antigravity tend to be more stable when you give several monetization models distinct roles rather than betting everything on one. Let me set out each one's character first.
Subscriptions (Monthly Billing)
Subscriptions generate predictable monthly recurring revenue (MRR), and the longer users keep receiving value, the higher their LTV grows. In exchange, how you reduce churn becomes the lifeline of the business. This model fits apps people touch every day — productivity tools, note apps, workflow utilities.
Free Plan
↓
Standard Plan ($4.99/month)
↓
Premium Plan ($9.99/month)
In-App Purchase (IAP)
The appeal of IAP is the flexibility of buying only the feature you need. A single purchase can deliver value for a long time, and it rides the App Store and Google Play native payment rails. Its purposes are broad — in-game currency, content sales, feature unlocks.
In practice, the easiest combination to operate is: acquire users with a free tier, turn core features into a subscription for stable revenue, and sell sharper features individually through IAP. Ads stay as a supplement to the free tier.
Free: core features + restrained ads
Standard ($3.99/mo): unlimited, team sharing up to 2, no ads
Premium ($7.99/mo): everything + AI assist + priority support
What Actually Guided Me When I Reconsidered the Model
At Dolice Labs, I run a Stripe membership across a set of technical blogs — a monthly Pro plan alongside a one-time, lifetime Premium plan. At first I assumed monthly alone would be enough. But once it was live, I found that a meaningful number of readers felt the recurring renewal as a psychological weight. When I added the one-time lifetime option, people who had hesitated at monthly finally moved.
What I learned from this is that a pricing table is not only a staircase of features — it is also a menu of ways to pay. The same value lands differently depending on whether you offer it monthly, as a one-time purchase, or as a single item. So in app monetization too, before committing to subscription-only, I now always ask whether I can slip in a one-time tier or a single IAP somewhere.
The other thing running a free-to-premium funnel hammered home is the obvious truth that cutting the entrance stops the revenue. A free tier is not a costly nuisance; it is the corridor that leads to paying. In an app, if you make the free tier too thin, the path to paid thins out with it. Offer an experience that genuinely helps for free, and place a natural "one more step" beyond it. In my experience, that design also turns out to be the most resistant to churn.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How to avoid the 'duplicated billing state' problem that breaks first when you combine subscriptions, IAP, and ads behind one backend
✦Webhook idempotency and retry design — preventing the double-charge accidents that quietly corrupt revenue
✦Building a funnel where a free tier is the entrance to paid, and reducing churn with self-serve design
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Billing should be anchored on the server side, where the secret key lives. The client only ever holds the publishable key.
Account Preparation
Create an account at stripe.com, get your API keys (publishable and secret), register a webhook endpoint, and run everything through Test mode first. Only move to Live mode once it is verified. Not touching the live key at the start is a humble but effective way to avoid accidents.
Environment Variables
Put the keys in the server's .env. Never include them in the client repository.
Billing breaks down into: press the button → create a session → pay on Stripe → confirm via webhook → raise the user's permission. Crucially, granting permission must happen on the webhook side — the moment the server receives the confirmed fact of payment. Never raise permission just because the browser reached success_url. Get this wrong and anyone who navigates straight to the success page without paying gets the entitlement.
Putting userId into metadata lets the webhook reliably trace "whose payment is this" later. It is a small move, but one that saves you repeatedly in operations.
Webhook Idempotency and Preventing Double-Counting
This is the spot where I first got burned in solo development. Stripe webhooks are "at least once" by design — the same event can arrive two or three times. If you write code that unconditionally adds one charge or bumps permission by one level on every receipt, network retries cause double-counting.
The fix is simple: record the event ID you received, and do nothing if it has already been processed. Combined with signature verification, this makes confirmation happen exactly once.
// server/routes/webhook.tsimport Stripe from 'stripe';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature'] as string; let event: Stripe.Event; // 1. Verify signature — reject forged requests try { event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!); } catch (err) { return res.status(400).send(`Webhook signature error: ${(err as Error).message}`); } // 2. Idempotency — never confirm the same event twice if (await db.hasProcessedEvent(event.id)) { return res.json({ received: true, duplicate: true }); } try { switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session; const userId = session.metadata?.userId; if (userId) await db.updateUser(userId, { plan: 'premium', status: 'active' }); break; } case 'customer.subscription.deleted': { const sub = event.data.object as Stripe.Subscription; const userId = sub.metadata?.userId; if (userId) await db.updateUser(userId, { plan: 'free', status: 'canceled' }); break; } } // 3. Mark processed, then return 200 await db.markEventProcessed(event.id); res.json({ received: true }); } catch (error) { // 4. On failure return 500 so Stripe retries (and do NOT record it) res.status(500).json({ error: (error as Error).message }); }});
The order matters. Run the confirmation, then call markEventProcessed, then return 200. If anything fails midway, return 500 to deliberately make Stripe resend, and reprocess on the next delivery. Do not reverse it into "record processed first, do the work later" — if the real work then fails, the event never comes again.
App Store / Google Play IAP Implementation
When selling digital goods inside a mobile app, platform-native billing is often required. Here we trigger purchases from StoreKit and Google Play Billing and verify the result on the backend. We always verify server-side because we should not trust a client's bare claim of "I bought it."
Once you support both subscriptions and IAP, you inevitably hit the problem that a user's billing state splits across two places. "premium" stands in Stripe and "premium" also stands in Google Play; cancel one and the app stays paid because the other is still alive. Conversely, two separate "canceled" notifications can arrive and strip away a right that was actually still valid.
The design I settled on is to consolidate the single source of truth for a user's entitlement in the app's own database, and treat both Stripe and IAP as inputs that update that database. Each path always carries a source so you can tell where a right came from. When a cancellation arrives, you expire only that source's right and leave the others untouched.
// server/lib/entitlement.tstype Source = 'stripe' | 'app_store' | 'google_play';// User is premium if at least one path grants itasync function recomputeEntitlement(userId: string) { const grants = await db.getActiveGrants(userId); // active records per source const isPremium = grants.some((g) => g.plan === 'premium' && g.status === 'active'); await db.updateUser(userId, { plan: isPremium ? 'premium' : 'free' });}// A cancellation on one source expires only that source's grantasync function revokeGrant(userId: string, source: Source) { await db.expireGrant(userId, source); await recomputeEntitlement(userId);}
Once I moved to "record per path, then sum and decide at the end," both double-counting and erroneous revocation stopped cold. Adding another billing path just adds one more term to the sum.
Self-Serve Cancellation and the Customer Portal
I understand the urge to hide the cancellation path when you badly want to reduce churn. But my experience runs the other way: the easier you make cancellation, the more trust remains, and the more people come back. Stripe offers a Customer Portal that lets users change plans, update payment methods, and cancel on their own. It cuts your support load and gives users the reassurance of managing their own wallet.
If you let people optionally leave a one-line reason on cancellation, the next thing worth fixing starts to show itself. Over the long run, that quiet ear for improvement works better than any retention trap.
A Hybrid Revenue Estimate (Just a Design Guide)
Finally, here is a sense of the revenue from combining several tiers, with placeholder numbers. This is not actuals — it is an estimate for deciding, at design time, how thick to make each tier.
Tier
Assumption
Approx. monthly revenue
Free (ads)
2,000 × $0.50
$1,000
Standard ($3.99)
2,000 users
$7,980
Premium ($7.99)
500 users
$3,995
IAP (one-time)
1,000/month
$2,000
More than the figures, look at the ratios. In most apps, the bulk of revenue is carried by a small slice of paying users. That is exactly why you design the free tier carefully as an entrance and return a density worth paying for to the paid tier. As long as that round trip holds, the numbers in the table tend to follow.
How to Read the Key Metrics
To optimize monetization you need to watch a few metrics over time. You don't have to chase all of them daily; anchoring on two — conversion rate (free → paid) and churn — makes it easy to judge whether a change is working.
Metric
Definition
Guide
Conversion Rate
Free → paid conversion
A few percent is one benchmark
Churn Rate
Monthly cancellation rate
Lower is better
LTV / CAC
Lifetime value ÷ acquisition cost
3x or more is healthy
MRR
Monthly recurring revenue
Keep it climbing
As a next step, I'd suggest implementing webhook idempotency first, before anything else. If you only notice double-counting after revenue starts flowing, reconstructing past consistency becomes a very heavy job. Once the foundation is firm, adding tiers one at a time is plenty. If this spares another solo builder a little of the detour I took, I'll be glad.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.