Architecture
RevFox is one process with the web panel embedded in it. It listens on two ports, so you can expose them under different hostnames and protect them separately.
| Server | Flag | Default | Serves |
|---|---|---|---|
| Panel | -addr | :8080 | The dashboard and the /api/… endpoints it uses. Sign-in required. |
| API | -api-addr | :8081 | Everything outside callers use: SDKs, Apple's notifications and your backend's server API. |
-target all, the default, runs both. -target web or -target api runs only one, for example to put the panel and the API server on different machines. Both use the same database: SQLite for a single server, or PostgreSQL.
The API server must be reachable from the internet, because apps and Apple call it. The panel only needs to be reachable by your team.
Who calls what
| Caller | Endpoint | Authenticated by |
|---|---|---|
| Your app, through the SDK | POST /sdk-events | The app's public SDK API key |
| Apple | POST /app-store-server-notifications/{id} | Apple's signature on the payload |
| Google Play | None. RevFox pulls from your Pub/Sub subscription. | Your service account |
| Your backend | /v1/customers/… | A secret API key |
| RevFox, calling your backend | Your webhook URL | An HMAC signature in X-Revfox-Signature |
| Load balancers | GET /api/health, GET /is_alive | None. Returns 204. |
The data model
- A project holds everything below. Members are added per project.
- An app is one store listing: an App Store bundle ID or a Play package name, with its store credentials and SDK API key.
- A store item is a product ID in one app. Benefits are what store items unlock, and packages are groups of store items a paywall shows.
- A customer is one of your users, known by the ID your app gives the SDK, plus any aliases.
- A transaction is one store payment or state change: a purchase, a renewal, a refund. Transactions of one subscription share an
original_transaction_id. Each has acustomer_id(who reported it) and anowner_id(who it entitles now). - An event is an entry in a customer's history. Events are what webhooks deliver.
A customer's benefits are worked out when they are read, from the transactions they own that are active now. No stored flag can drift out of step with the store.
The life of a purchase
- The app fetches a package. RevFox answers with that platform's product IDs, and the SDK loads titles and prices from the store, so the price shown is always the store's.
- The customer buys through StoreKit or Play Billing.
- The SDK sends the store's proof of purchase to
/sdk-events: StoreKit 2's signed transaction, or the Play purchase token. - RevFox verifies it. For Apple it checks the signature chains to Apple's root certificates and the bundle ID matches. For Google it looks the token up with the Android Publisher API. See Purchases and revenue.
- RevFox fetches the subscription's history from the store and records every payment it was missing, so a reinstall or a new device still yields complete history.
- It answers with the customer's benefits. Only then does the SDK finish the StoreKit transaction or acknowledge the Play purchase.
- The purchase is added to the customer's history and queued for webhooks and analytics.
When the network fails after the store has charged, the SDK still reports success, grants access from what the device can prove, and leaves the transaction unfinished. The store offers it again on the next launch and the SDK reports it then.
Keeping subscriptions current
After the purchase, renewals, cancellations, billing problems and refunds happen at the store. RevFox learns about them in four ways, and each change is recorded once, whichever way reports it first.
| Source | How |
|---|---|
| App Store Server Notifications | Apple calls the app's notification URL. Only Apple-signed payloads are accepted. |
| Play real-time developer notifications | RevFox pulls the Pub/Sub subscription every 30 seconds, then reads the subscription from the Android Publisher API. |
| Subscription refresher | RevFox asks the store for the state of each subscription when a renewal is due and at least once a day. This catches notifications that were never set up or were lost. |
| Play voided purchases | Every 30 minutes RevFox reads Google's list of refunded and charged-back purchases from the last 30 days. |
Background jobs
RevFox runs these inside its own process. Nothing else needs to be scheduled.
| Job | Runs | Does |
|---|---|---|
| Play Pub/Sub | Every 30 s, configurable | Pulls and acknowledges real-time developer notifications for every connected Play app. |
| Subscription refresher | Every minute | Checks the subscriptions that are due with their store. |
| Play voided purchases | Every 30 min | Records refunds Google reports. |
| Import worker | Continuously | Processes queued migration rows. |
| Apple Search Ads | Every minute | Exchanges AdServices tokens with Apple for campaign attribution. |
| Webhook dispatcher | Continuously | Delivers events and retries failures after 5, 10, 20, 40 and 80 minutes. |
Security model
- Purchases need store proof. An App Store purchase must come with a transaction signed by Apple; a bare transaction ID is refused, since anyone can read one. A Play purchase token is checked with Google.
- The SDK API key is public. It ships in your app, so treat it as an app identifier, not a password. It cannot read or change other projects or apps. Anyone holding it can ask for the benefits of a customer ID they know, so do not use guessable customer IDs such as email addresses or sequential numbers.
- Secret keys stay on servers. A secret key can read and change every customer in its project. It is shown once and only its hash is stored.
- Webhooks are signed. Check
X-Revfox-Signaturebefore trusting a delivery. See verifying deliveries. - The panel is separate. It uses sign-in sessions and per-project
readandwriteroles. Sign-in attempts are rate-limited.
Configuration
Pass a config file with -config. Environment variables override it.
[database]
; sqlite or postgres
type = sqlite
path = data/revfox.db
; for postgres, a URL, or host, port, name, user, password and ssl_mode
; url = postgres://revfox:[email protected]:5432/revfox?sslmode=require
[server]
; public URL of the API server
api_url = https://api.example.com
[play_pubsub]
poll_interval_seconds = 30
max_messages = 10
Comments start a line with # or ;. A comment after a value on the same line becomes part of the value.
| Setting | Environment variable |
|---|---|
[database] type, path, url | REVFOX_DATABASE_TYPE, REVFOX_DATABASE_PATH, REVFOX_DATABASE_URL |
[database] host, port, name | REVFOX_DATABASE_HOST, REVFOX_DATABASE_PORT, REVFOX_DATABASE_NAME |
[database] user, password, ssl_mode | REVFOX_DATABASE_USER, REVFOX_DATABASE_PASSWORD, REVFOX_DATABASE_SSL_MODE |
[server] api_url | REVFOX_API_URL |
[play_pubsub] poll_interval_seconds, max_messages | REVFOX_PLAY_PUBSUB_POLL_INTERVAL_SECONDS, REVFOX_PLAY_PUBSUB_MAX_MESSAGES |
-log-level flag | REVFOX_LOG_LEVEL: debug, info, warn or error |
| First admin password | REVFOX_ADMIN_PASSWORD, read only when the database is new |
api_url is what the panel shows as the SDK's base URL and as each App Store app's notification URL.
