onesub
Server-side receipt validation for react-native-iap. One line.
react-native-iap handles the purchase.
onesub handles everything after — validation, webhooks, subscription state.
Open source. Self-hosted. Zero revenue share.
The Problem
You use react-native-iap to handle purchases. But then you need a server to:
- Verify Apple StoreKit 2 receipts (JWS signature validation)
- Verify Google Play receipts (Play Developer API v3)
- Handle webhooks (renewals, cancellations, refunds)
- Track subscription state in a database
- Track one-time purchases (consumables + non-consumables)
- Expose "is this user subscribed?" and "what did this user buy?" endpoints
That's 2-3 weeks of work. Or one line:
app.use(createOneSubMiddleware(config));
How It Works
react-native-iap (client) @onesub/server (your backend)
┌─────────────────────────────────┐
Subscriptions: │ │
requestSubscription() ──receipt───▶ │ POST /onesub/validate │
fetch('/onesub/status') ──────────▶ │ GET /onesub/status │
│ │
One-time purchases: │ │
requestPurchase() ────receipt───▶ │ POST /onesub/purchase/validate │
fetch('/onesub/purchase/status') ──▶│ GET /onesub/purchase/status │
│ │
Webhooks (auto): │ POST /onesub/webhook/apple │
│ POST /onesub/webhook/google │
└─────────────────────────────────┘
Quick Start
1. Install
npm install @onesub/server
2. Add to your Express app
import { createOneSubMiddleware, PostgresSubscriptionStore } from '@onesub/server';
app.use(createOneSubMiddleware({
apple: {
bundleId: 'com.yourapp.id',
sharedSecret: process.env.APPLE_SHARED_SECRET,
},
google: {
packageName: 'com.yourapp.id',
serviceAccountKey: process.env.GOOGLE_SERVICE_ACCOUNT_KEY,
},
database: { url: process.env.DATABASE_URL },
store: new PostgresSubscriptionStore(process.env.DATABASE_URL),
}));
3. Check subscription from your app
const res = await fetch('https://api.yourapp.com/onesub/status?userId=user123');
const { active } = await res.json();
// active: true → subscribed, false → not subscribed
Done. Apple/Google receipt validation, webhooks, and subscription tracking — all handled.
What You Get
Subscriptions (auto-renewable)
| Endpoint | What it does |
|---|---|
POST /onesub/validate | Verify receipt, save subscription |
GET /onesub/status?userId= | Check if user has active subscription |
POST /onesub/webhook/apple | Handle App Store Server Notifications V2 |
POST /onesub/webhook/google | Handle Google Real-Time Developer Notifications |
One-time Purchases (consumable + non-consumable)
| Endpoint | What it does |
|---|---|
POST /onesub/purchase/validate | Verify receipt, save purchase |
GET /onesub/purchase/status?userId= | List user's purchases |
Consumables (coins, credits): Can be purchased multiple times. Each purchase is tracked.
Non-consumables (unlock premium, remove ads): Purchased once. Duplicate purchases are rejected with 409 NON_CONSUMABLE_ALREADY_OWNED.
// Validate a consumable purchase
const res = await fetch('https://api.yourapp.com/onesub/purchase/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
platform: 'apple',
receipt: transactionReceipt,
userId: 'user123',
productId: 'credits_100',
type: 'consumable', // or 'non_consumable'
}),
});
const { valid, purchase } = await res.json();
What's Under the Hood
- Apple: JWS signature verified against Apple JWKS (not just decoded)
- Google: OAuth2 service account → Play Developer API v3, token cached
- Webhooks: Auto-handle renewals, cancellations, expirations, refunds
- Storage: Pluggable
SubscriptionStore— built-in PostgreSQL + in-memory - Validation: zod input validation, 50KB body limit, userId length checks
- Security: Full details →
Optional: React Native SDK
If you want a drop-in React hook + paywall component (built on react-native-iap):
npm install @onesub/sdk react-native-iap
import { OneSubProvider, useOneSub } from '@onesub/sdk';
// Wrap your app
<OneSubProvider config={{ serverUrl, productId }} userId={userId}>
<App />
</OneSubProvider>
// Subscriptions
const { isActive, subscribe, restore } = useOneSub();
// One-time products (consumable / non-consumable)
const { purchaseProduct } = useOneSub();
// Purchase a consumable (e.g. coins)
const purchase = await purchaseProduct('credits_100', 'consumable');
// purchase is null if user cancelled, PurchaseInfo on success
// Purchase a non-consumable (e.g. premium unlock)
const purchase = await purchaseProduct('premium_unlock', 'non_consumable');
The SDK is optional. You can use @onesub/server with any client — React Native, Flutter, or plain HTTP calls.
Optional: MCP Server (AI Integration)
For Claude Code / Cursor users — AI helps set up your subscription:
{ "mcpServers": { "onesub": { "command": "npx", "args": ["@onesub/mcp-server"] } } }
"Add a monthly subscription to my Expo app"
Packages
| Package | Version | What | Install |
|---|---|---|---|
@onesub/server | Express middleware — receipt validation + webhooks | npm i @onesub/server | |
@onesub/sdk | React Native SDK — useOneSub() + <Paywall /> | npm i @onesub/sdk | |
@onesub/mcp-server | MCP tools — AI creates products + paywalls | npx @onesub/mcp-server | |
@onesub/shared | Shared TypeScript types | Auto-installed |
vs RevenueCat
| RevenueCat | onesub | |
|---|---|---|
| Receipt validation | Their servers | Your server |
| Revenue share | 1% after $2.5K | 0% forever |
| Data ownership | Their database | Your database |
| Vendor lock-in | Yes | No (MIT open source) |
| Dashboard | Yes | Not yet |
| Setup time | 2-3 hours | 10 minutes |
onesub is not a RevenueCat replacement. RevenueCat offers analytics, experiments, and a dashboard. onesub is for developers who want to own their subscription infrastructure.
Examples
Working examples to get you started in minutes:
| Example | What | Run |
|---|---|---|
examples/server | Express server with receipt validation | npm start |
examples/expo-app | Expo Router app with paywall | npx expo start |
# 1. Start the server
cd examples/server
cp .env.example .env # add your Apple/Google credentials
npm install && npm start
# 2. Start the app (in another terminal)
cd examples/expo-app
npm install && npx expo start
Custom Store
Built-in PostgreSQL store, or bring your own:
import { SubscriptionStore } from '@onesub/server';
class RedisStore implements SubscriptionStore {
async save(sub) { /* ... */ }
async getByUserId(userId) { /* ... */ }
async getByTransactionId(txId) { /* ... */ }
}
app.use(createOneSubMiddleware({ ...config, store: new RedisStore() }));
Roadmap
- Apple StoreKit 2 receipt validation (JWKS verified)
- Google Play Billing v3 receipt validation
- Webhook handlers (Apple V2 + Google RTDN)
- PostgreSQL subscription store
- React Native SDK + paywall components
- MCP server for AI-assisted setup
- Security hardening (zod validation, body limits, signature verification)
- CLI scaffolding (
npx onesub init) - Analytics dashboard
- Hosted service (no server needed)
Contributing
git clone https://github.com/jeonghwanko/onesub.git
cd onesub && npm install && npm run build && npm test
See CLAUDE.md for architecture and conventions.
License
react-native-iap handles the purchase.
onesub handles everything after.