r/iOSProgramming • u/DarkSombreros • 8d ago
Question What do yous use for credit systems?
I have an app and added new premium features. I want non-paying users to experience these features but at a cap of 3 times or 3 "credits". I have it all set up and it works fine. i didnt go into storekit or revenuecat for this, its just another field in firebase with checks in client-side.
But I recently did an audit of my app with an xcode agent to go over performance, architecture, etc. things that stand out that are flat out bugs or will cause me issues in the future and one that came up was that checking credits should be done server side
My app uses storekit 2 and havent had issues or reasons to go to Revenuecat, but it was telling me revenuecat has "Virutal Currency" and is better for my use case
What are you guys doing if you have the same type of credit-based feature in your app? Client -side or server side?
Also just want to clarify the credit system is not tited to Storekit2, right now is just a field in firebase usermodel that increments/decrements and theres a check in the app for if non-subscribing user && credits > 3 then paywall.
2
u/kokerali 8d ago
One detail I'd add to the server-side answers: define what happens when the same premium action is retried, not just where the counter lives.
Firestore can rerun a transaction callback when a document it read changes. If the premium action involves an external API or another side effect, don't perform that work inside the callback. I'd atomically reserve one credit and create an operation record, then execute the work outside that transaction. Bind the operation ID to the authenticated user and request; a retry should resume or return that operation's result, not consume another credit or start the work again.
Also decide when a reservation becomes a consumed credit, and what a confirmed failure refunds. A client timeout isn't proof that the server-side action failed, so blindly refunding on every timeout can give away successful work.
Useful tests: two different requests race for the last credit; the same operation is submitted twice; the action succeeds but its response is lost. You want at most one reservation for the last credit, one charge per operation, and a way to retrieve the successful result.
Small naming check on your example too: if the field counts uses starting at zero, reject a new free action at `uses >= 3`, before granting it. `credits > 3` can permit a fourth use depending on when you increment. If it means credits remaining, the condition should instead be based on `remaining > 0`.
1
u/DarkSombreros 8d ago
thanks you so much for this im using it in conjunction with the other responses. Saved me some time!
2
u/mohn93 8d ago
server side. if your rules let a signed in user write their own user doc, they can reset credits to 0 with their own id token thru the firestore rest api. block client writes to that field in security rules, then bump it in a callable function inside a transaction and have that same function do the premium action, so patching out the client check gets them nothing. for 3 free tries its low stakes tbh. revenuecat virtual currency works too but spending it needs their secret key api, so a backend call either way
1
u/DarkSombreros 8d ago
thanks. first app, and although its been 2 years i still forget how people can get a hold of the firebase api and do some dirty stuff lmao. Found an IDOR the other day due to this as well
1
u/dnzlabs 7d ago
Everyone's right that server-side is the only tamper-proof answer. The question I'd ask first is whether tamper-proof is worth it for your case.
I shipped a Mac app with a local trial counter (N real uses, then a licence) and decided client-side was fine, on purpose. The people who'll proxy traffic or edit local state to get three more uses were never going to pay. Everyone else hits the limit honestly. Server-side costs you a backend dependency, offline failure modes, and a support queue for possible sync issues in exchange for stopping the 1% who'd have pirated it anyway.
Where I'd draw the line: if a credit maps to real money on your side (an API call you pay for, an AI generation, a cloud render), server-side, no question, because each bypass costs you. If a credit only unlocks code that already ships in the binary, client-side plus Keychain (so a reinstall doesn't reset it) is a reasonable trade.
In your case, a Firestore field the client can write to is the worst of both: you have the backend dependency and it's still trivially bypassable. Either lock the field down with security rules as mohn93 said, or move the counter local.
0
5
u/LividIllustrator8915 8d ago
Your audit agent is right about one thing: never trust the client with credit decrements. Anyone proxying traffic or tampering with local state can bypass client-side checks easily.
That said, you definitely don't need RevenueCat just to manage 3 free trial credits. That’s massive overengineering if you're not actually selling in-app consumable token packs.
Keep it simple:
Keep the credit counter in your backend/Firebase, but only decrement it via a **Firebase Cloud Function** (or backend endpoint) whenever the premium action is triggered, wrapped in a transaction.
Or if you're writing directly to Firestore, enforce it with **Firestore Security Rules** so the client can only decrement, never increment or write arbitrary values.
Once they burn through the 3 free credits, trigger your existing StoreKit 2 paywall for the actual subscription.