Draft — not yet published
Structure is in place; several technical specifics still need to be verified against the real implementation before this goes live. Delete <DraftBanner /> once confirmed.
Case study · RemoteDev Limited · – Present
Three payment providers, one subscription state
Protect-A-Plate lets UK drivers register a number plate and get alerted when someone tries to have a duplicate manufactured. It runs on web, iOS and Android, serves six different user roles, and bills through three payment providers that disagree with each other about almost everything.

- Constraint
- One subscription state across web, iOS and Android — where Apple and Google refuse to let you bill their users through Stripe.
- What broke
- Duplicate in-app purchase transactions and token-refresh loops hitting live subscribers in production.
- What changed
- One entitlement resolver behind all three providers; the rest of the system stopped knowing where a subscription came from.
The constraint
Plate cloning is a real and specific problem — the platform cites over a thousand reports a month in the UK. A cloned plate means someone else's fines, tolls and speeding tickets arrive at your address, and the burden of proof lands on the innocent driver.
The product had to serve six roles that want completely different things from the same data: drivers registering their own plates, plate suppliers checking a registration before manufacturing, fleet operators managing hundreds of vehicles, insurers investigating claims, verifying a plate at the roadside, and internal admin.
The hard part was never the feature list. It was that all six roles, on three platforms, had to agree at all times on one question: what is this account currently entitled to?
Why the obvious approach fails
The obvious approach is to let each platform own its own billing — Stripe records web subscriptions, Apple records iOS ones, Google records Android ones — and check whichever one the user is currently on.
That breaks the moment a real user behaves like a real user. Someone subscribes on the web, installs the iOS app, and the app has no idea they're a paying customer. Someone subscribes on Android and opens the web dashboard to see nothing. Someone cancels on one platform while an auto-renewal is still live on another. Each provider has its own idea of when a subscription lapses, its own grace-period semantics, and its own retry behaviour on failed payments.
Three sources of truth about the same fact is not a payments problem. It's a distributed-state problem wearing a payments costume.
The decision, and what it replaced
The original model billed per plate. It was simple to implement and wrong for the business: it punished exactly the customers worth having — fleet operators with dozens of vehicles — and it made entitlement a per-object question, which multiplied against three providers into a state space nobody could reason about.
I replaced it with four recurring tiers — Basic, Pro, Premium and Fleet — and moved entitlement from the plate to the account. That collapsed the question from “is this plate paid for, on this platform” to “what tier is this account on”, which is a single value that one service can own.
Everything downstream reads that one resolved tier. No route, screen or portal contains the word Stripe, Apple or Google.
The hard part: duplicates in production
None of the three providers guarantee exactly-once delivery of purchase notifications, and all three retry. Under normal load you never notice. Under real load, with real network conditions and real retries, you get the same purchase delivered more than once — and if your write path is naive, one purchase becomes two entitlement records, or a stale one overwrites a fresh one.
This surfaced as duplicate in-app purchase transactions affecting live subscribers, alongside a separate token-refresh loop where clients could get stuck re-requesting credentials. Both are the kind of bug that is invisible in staging and expensive in production, because they need concurrency and real provider behaviour to reproduce.
The fix is not on the write — it's in front of it. Every provider notification is keyed on the provider's own transaction identifier and checked before anything is written, so a redelivered notification is a no-op rather than a second subscription.
Encrypting VINs without losing search
Vehicle identification numbers are personally identifying and had to be encrypted at rest. But the platform's entire purpose depends on detecting when the same vehicle appears twice — which normally requires reading the plaintext you just encrypted.
The resolution is to store two derived values rather than one: AES-256-GCM for the encrypted VIN itself, and a separate HMAC-SHA256 blind index for equality lookups. Because HMAC is deterministic under a fixed key, two identical VINs produce the same index and collide on lookup — while the index itself reveals nothing about the value, and the encrypted field stays authenticated and non-deterministic.
Duplicate detection therefore never touches a plaintext VIN, and the database never holds one.
Outcome
The platform is live and in production — web at protect-a-plate.co.uk, with the mobile app shipped to both the App Store and Google Play through a GitHub Actions and EAS Build/Submit pipeline.
The concrete result of the entitlement work is negative rather than numeric: subscription-state conflicts across providers stopped happening, and the duplicate-transaction and token-refresh incidents affecting live subscribers were resolved.
What I can't claim: I don't have clean before/after instrumentation isolating the entitlement rewrite from the other changes shipping in the same window, so I'm not going to attach a percentage to it.
What I'd change
The idempotency check and the entitlement write live close enough together that the ordering guarantees are implicit rather than enforced. If I were starting again I'd make the provider notification an explicit queued event with the deduplication as a separate stage, so replaying the entire notification history against a rebuilt entitlement table is a routine operation rather than a migration.
I'd also invest earlier in a provider-sandbox test harness. Both production incidents were reproducible only under concurrency, and the cost of not being able to force those conditions in CI was paid in production.