RORK LABJP
R8 — From Expo SDK 58, R8 is enabled by default for Android release builds. Measuring the same code before and after shows both what shrank and what brokeXCODE26.6 — The Xcode 27 image for EAS Build is still coming soon, and latest is still Xcode 26.6. Which bugs you hit depends on whether your local Xcode is already on 2711/01 — Extension requests for the Google Play target API level close on November 1, forty days away. New and updated apps need API 36 or higher, existing apps API 35 or higherFETCH — expo/fetch is reported to hang without settling on iOS, while Android resolves a truncated body as a plain 200. Nothing throws, so the timeout has to be yoursNEW — A build that stops at exit code 0: the flag that silenced the logs, and the check that let an empty file throughSCENE — expo@57.0.23 added an opt-in for launching under Xcode 27 while staying on SDK 57. Enable ios.enableSceneSupport through expo-build-propertiesR8 — From Expo SDK 58, R8 is enabled by default for Android release builds. Measuring the same code before and after shows both what shrank and what brokeXCODE26.6 — The Xcode 27 image for EAS Build is still coming soon, and latest is still Xcode 26.6. Which bugs you hit depends on whether your local Xcode is already on 2711/01 — Extension requests for the Google Play target API level close on November 1, forty days away. New and updated apps need API 36 or higher, existing apps API 35 or higherFETCH — expo/fetch is reported to hang without settling on iOS, while Android resolves a truncated body as a plain 200. Nothing throws, so the timeout has to be yoursNEW — A build that stops at exit code 0: the flag that silenced the logs, and the check that let an empty file throughSCENE — expo@57.0.23 added an opt-in for launching under Xcode 27 while staying on SDK 57. Enable ios.enableSceneSupport through expo-build-properties
Articles/Business
Business/2026-09-22Advanced

A successful payment never tells you what to unlock. Routing four products through one Checkout

Tips, single articles, monthly plans and lifetime access all came back from Stripe looking identical. Here is how I moved entitlement off the amount and onto metadata, and why I now fail closed when the entitlement store goes quiet.

Stripe18Monetization38EntitlementsCloudflare Workers25Billing

Premium Article

I was rereading the post-payment handler late one evening, about to add a fourth product. What I found was a single straight line: if the payment completed, write a membership record.

Back when there was exactly one product, that line was correct. Adding a tip, then a monthly plan, then a single-article purchase put the same line in danger three separate times.

The thing worth saying first is that a successful payment only tells you that money moved. Who may see what is something you decide when you create the session, not something the payment processor hands back.

The amount answers "did they pay". The declaration made at creation time answers "what may they open". Keeping those two out of the same if is the one rule I try not to bend.

Every product I added changed what success meant

The site I run has four payments with quite different personalities: a small tip, a single-article purchase, a monthly plan, and a one-time lifetime unlock.

They all enter through one Checkout endpoint. Keeping one entrance means the return URL, the locale and the cancel path are handled in a single place.

The exit was the problem. All four come back with payment_status: paid, so from the receiving side they wear the same face.

ProductmodeWhat it grantsRetention
Tip ($1.50)paymentNothing at all
Single article ($1.50)paymentThat one slug10 years
Pro ($5/mo)subscriptionMembership31 days, renewed
Lifetime ($15)paymentMembership10 years

The first row is the easiest one to get wrong. Nobody sets out to write code where a $1.50 thank-you buys lifetime access. It happens because one older line still says "paid, therefore member".

Keep "they paid" and "they may open this" in separate fields

The temptation to branch on the amount deserves an honest answer, because $1.50 and $15 really are easy to tell apart.

Amounts move, though, and they move for reasons that have nothing to do with access. The day I added a thank-you price, the day I lowered the single-article price, and the day the same product appeared in a second currency, an amount-based table would have needed rewriting each time.

A declared type does not move. "This is a tip" means the same thing after a price change and in any currency.

So the type is fixed at creation and carried out past the payment layer. With Stripe that container is the Checkout Session's metadata field, described in the Checkout Session API reference.

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
You'll be able to rewrite a Stripe Checkout flow that turns payment success straight into access, so that entitlement comes from metadata instead of the amount
You'll be able to catch the case where a small tip or a single-article purchase silently unlocks a higher plan, before it reaches production
You'll be able to decide which way to fail when your entitlement store is briefly unreachable, instead of writing let them through and moving on
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.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork 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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Business2026-06-28
Don't Pay Out a Rewarded Ad on the Client's Word Alone — SSV Verification for a Rork (Expo) App on a Worker
Trusting the client-side 'reward earned' callback alone invites double-grants and spoofing. Here is how to wire AdMob server-side verification (SSV) into a Rork-generated Expo app, verify the signed callback on a Cloudflare Worker, and make payouts idempotent with transaction_id.
Business2026-07-12
Lower Store Fees by Their Structure, Not Their Rate — Apple SBP and Google Play's June 2026 Pricing
A working breakdown of Apple's Small Business Program and Google Play's new fee model that started June 30, 2026, framed as fee design for indie developers, with code to compute your effective rate and a break-even table.
Business2026-07-05
When Your AdMob Earnings Suddenly Get Deducted: Preventing Invalid Traffic as a Solo Developer
Invalid traffic deductions in AdMob are unsettling because the cause is rarely obvious. From the perspective of running several apps solo, here is a minimal setup that prevents the most common accidents, plus how to respond when a deduction actually happens.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links