I finally built the in-app purchase tool I wish I had years ago
Native payment sheets on iOS and Android. Subscription data in your Rails database.
Hey friends, I’m finally shipping something I’ve wanted to build for years.
Say hi to PurchaseKit: in-app purchase infrastructure for Hotwire Native apps.
You can now add subscriptions to your Hotwire Native app without writing any native code or worrying about keeping your database in sync with Apple and Google.
I’ve been adding some version of this to client apps for years. And every single time, it’s a huge pain for me and my clients. StoreKit on iOS. Play Billing on Android. Webhook verification. Receipt validation. Subscription lifecycles…
And the worst part? Keeping your Rails database in sync with what Apple and Google think the subscription status is.
I’ve talked to so many developers who don’t go native specifically because they don’t want to deal with this stuff. Apple and Google require in-app purchases for certain apps, and the complexity just isn’t worth it.
So I finally built something generic that everyone can use. I’ve launched over 25 Hotwire Native apps. This is the billing infrastructure I use.
How it works
Native packages for iOS and Android (bridge components) handle the purchase flow. StoreKit 2 on iOS, Play Billing on Android. Your Rails app never touches the native payment sheets directly. And you never touch the native code.
You write your paywall in ERB. Style it like the rest of your app. And because it’s server-rendered, you can deploy pricing experiments without pushing new binaries to the app stores.
<%= purchasekit_paywall customer_id: current_user.id do |paywall| %>
<%= paywall.plan_option product: @annual, selected: true do %>
Annual - <%= paywall.price %>/year
<% end %>
<%= paywall.plan_option product: @monthly do %>
Monthly - <%= paywall.price %>/month
<% end %>
<%= paywall.submit “Subscribe” %>
<% end %>Webhooks are normalized and forwarded to your Rails app. Apple and Google send completely different webhook formats. PurchaseKit normalizes them into one consistent structure before forwarding to your server.
Your subscription data lives in your database. Not on Apple’s servers. Not on Google’s servers. In your database, right next to everything else.
PurchaseKit keeps everything in sync via webhooks. When a subscription renews, cancels, or expires, your database updates automatically. If you’re using Pay, it creates and updates Pay::Subscription records. If not, use event callbacks to handle it however you want.
PurchaseKit.configure do |config|
config.on(:subscription_created) do |event|
user = User.find(event.customer_id)
user.subscriptions.create!(...)
end
endYour current_user.subscribed? checks just work. No API calls. No stale data.
Pricing
I’m launching PurchaseKit with a free tier so you can try it out and validate your idea before paying anything.
Free - Up to 10 paying customers for 1 app. No credit card required. This is enough to build your paywall, test the full flow in sandbox, and launch to real, paying users.
Pro ($99/mo) - Unlimited paying customers for up to 3 apps. For most indie developers and small teams, this is the plan.
Business ($399/mo) - Unlimited everything, plus concierge onboarding where I personally help you get set up.
January launch offer
I want to make sure early adopters have a great experience. So here’s the deal:
If you create an account in January, I’ll personally onboard you. We’ll do a video call to set up your iOS, Android, and Rails code. We’ll walk through App Store Connect and Google Play integration. You’ll walk away with a working paywall in your app.
This is normally a Business tier perk ($399/mo). For January, it’s free. Even on the Free plan. Just reply to this email to get on my calendar.
Next week I’m publishing a deep dive on in-app purchases: all the moving pieces, the gotchas, and why this stuff is so tricky. It’ll give you a much better sense of what PurchaseKit handles under the hood.
For now, check out PurchaseKit and let me know what you think.
Questions? Just reply to this email. I can’t wait to see what you build!



This is really elegant work. The way you normalize Apple and Google's conflicting webhook formats into one consistnt structure is the smart move that saves devs weeks of debugging. Most ignore this but you're right that mismatched webhok schemas across platforms cause silent subscription state divergence. From experience, most billing bugs come from this exact layer, not the payment logic itself.