What UPG is and why we built it
UPG (Unified Payment Gateway) is a Synterra product: a multi-tenant subscription and entitlement service. It owns the integration with every payment rail, turns them into one event contract and one state machine, and calls back into each product's backend. Read this before you add payments to any product, or before you propose calling a payment provider directly.
UPG is not a payment processor. It never moves money, never sees a card number, and never holds funds. The rails do that.
How it works
- A tenant is a company using UPG. An app is one product surface of that tenant, in one environment. Test and live are two app rows.
- A connection is one provider account: one Apple app, one Play app, one Stripe account. It holds the credentials, the webhook route and the product ids.
- The product backend tells UPG who the user is with an opaque
user_ref. UPG holds no email, phone or name.
Why build it instead of buying or calling rails directly
| Option | Why not |
|---|---|
| Each app calls Apple, Google and Stripe itself | Every app re-implements four signature schemes, about 45 provider event types, four state machines, retries, reconciliation, and every yearly breaking change |
| RevenueCat, Adapty, Qonversion | No support for the India and GCC gateways and India's e-mandates, which were our biggest markets. We would still hand-build those rails and have two sources of truth. Also priced as a percentage of revenue |
| Kill Bill | An active billing engine that owns the clock and the invoices. We need a passive receiver of state that Apple, Google and the gateways already decided. No in-app purchase plugins |
| Hyperswitch, Lago, Polar | Useful ideas, no in-app purchases |
What we took from prior art: keep entitlements separate from subscriptions, and do
multi-tenancy as logical instances in one deployment. What we used instead of writing
it: Apple's app-store-server-library for certificate-chain verification.
Be honest about the cost. Rail adapters are a permanent treadmill. About once a year each rail changes something that breaks: Apple moved receipts to the App Store Server API and notifications to v2, Google forces a new Billing Library and broke subscriptions v1 to v2, and India's rules move. That ongoing work is what a vendor's percentage buys. We pay it once, in UPG, for every product.
The design rules
UPG is product-agnostic. Three tests: rename the first tenant with sed and nothing
breaks; a second tenant needs no code change; adding an account, route, plan or rail
needs no deploy. Product-specific data goes in metadata, never in a column.
Entitlement names are strings UPG never interprets. UPG says "this user holds pro".
The product decides what pro unlocks and how much of it.
A tenant's read path never calls UPG synchronously. Products keep their own entitlement table, filled by callbacks. If UPG is down, products keep working.
No personal data. user_ref is opaque. A database check refuses values that look like
an email, and validation refuses phone- and name-shaped values. Erasure pseudonymises
user_ref and keeps the financial rows.
Who owns what
| UPG owns | The product owns |
|---|---|
| Plans, prices per country, billing period, trial | Features and what each entitlement unlocks |
| Routing: which rail serves which surface and country | Limit values, usage counting, metering |
| Plan groups (tiers) and upgrade rules | Plan display text and marketing copy |
| Entitlement names | The paywall and its local entitlement table |
| Subscription state, money ledger, refunds | Sending invites and emails |
Where to go next
- Integrating a product backend: what your service builds.
- Inside UPG: the pipeline, data model and rules, for people working on UPG itself.