Data model
Kasskeep stores everything as tenant → app → user / transaction. Your account is a tenant, and a tenant can have many apps.
Tenant
Section titled “Tenant”A tenant is an organization: a company, a team or a single developer. It owns apps, members and an audit log.
- Every business table carries a
tenant_id. Queries filter by it, and Postgres row-level security enforces it as a second line of defense, so a missing filter in code still cannot read another tenant’s rows. - Store credentials and webhook secrets are encrypted with AES-256-GCM. The key for each tenant is derived from a master key with HKDF-SHA256, and the tenant id is bound as additional authenticated data, so ciphertext copied to another tenant’s row does not decrypt.
- Background jobs (webhook delivery, store acknowledgements, reconciliation) are scheduled round-robin across tenants with a per-round cap, so one tenant’s backlog cannot starve the others.
- App-facing endpoints are rate limited per tenant; over the limit they return
429 rate_limited. A suspended tenant receives403 tenant_suspended.
Tenant ids look like ten_....
An app is one product that sells things, for example a game that ships on iOS, Android and the web. An app has:
- its store identifiers (
ios_bundle_id,android_package, the App Store app id); - channel credentials: Apple, Google, Stripe and ad networks;
- a
webhook_urland the three keys described in Keys and access; - switches such as the test store and test ads.
App ids look like app_... and appear in every store notification URL.
A user is identified by your own app_user_id. Kasskeep does not create accounts; it records which of your users owns which purchases.
- The server accepts any string of 1 to 200 characters. The React Native SDK requires a UUID, because Apple’s
appAccountTokenmust be a UUID. - On iOS the SDK sets
appAccountTokento the user id, and on AndroidobfuscatedAccountId. A transaction that carries a different id is rejected with403 app_account_token_mismatch, and one already bound to another user with403 transaction_belongs_to_another_user.
GET /v1/customers/:app_user_id returns the user’s subscriptions and purchases. See Customers.
Transaction and subscription
Section titled “Transaction and subscription”A transaction is one store charge: a one-time purchase or one subscription period. It is unique per app, platform, environment and store transaction id, so the same purchase submitted twice is recorded once.
A subscription is the current state of a renewal chain, keyed by its original transaction id. Its status is one of:
| Status | Meaning | Access |
|---|---|---|
active |
In the current period, renews automatically | Yes |
canceled |
In the current period, auto-renew turned off | Yes, until the period ends |
grace_period |
Payment failed, inside the grace period | Yes |
billing_retry |
Payment failed, the store is still retrying | No |
expired |
Ended | No |
revoked |
Refunded or family sharing revoked | No |
paused |
Android only, paused by the user | No |
One-time purchases have status owned, refunded or revoked.
Every change Kasskeep learns about becomes an event, such as purchase, subscription.renewed or refund. Events are signed and POSTed to your webhook URL and can also be read in order from GET /v1/events. The event id is derived from the store event itself, so the same renewal reported by a notification, a client submission and reconciliation always has the same id. See Event types.
Product and entitlement
Section titled “Product and entitlement”Products are identified by the store’s product id (product_id). For Stripe, which has no product id of its own in this sense, you map your product id to a Stripe price with product mapping, so the same product_id can be sold on the App Store, Google Play and the web.
Planned Entitlements: defining access such as “pro” once and mapping it to products on every channel is on the roadmap. Today your backend maps product_id to access.