MIT / self-hosted셀프 호스팅 / 0% revenue share수수료 0% / Apple + Google

react-native-iap handles the purchase. onesub handles everything after.

결제까지는 react-native-iap. 그 다음 전부는 onesub.

Receipt validation, store webhooks, subscription lifecycle, and entitlements — as one Express middleware running on your server, against your database. A React Native SDK ships with it; Unity, Flutter, and plain HTTP clients work just as well.

영수증 검증, 스토어 웹훅, 구독 라이프사이클, entitlement — 당신의 서버에서 당신의 DB에 대고 도는 Express 미들웨어 하나로 끝난다. React Native SDK가 함께 오지만 Unity·Flutter·순수 HTTP 클라이언트로도 똑같이 쓴다.

$ npm i @onesub/server
GitHub Docs문서

The part react-native-iap doesn't do

react-native-iap이 해주지 않는 부분

The client library opens the store sheet and hands you a receipt. Everything that makes a subscription actually work is server-side — and none of it ships in the box.

클라이언트 라이브러리는 스토어 결제창을 띄우고 영수증을 넘겨줄 뿐이다. 구독을 실제로 굴러가게 만드는 건 전부 서버 쪽 일이고, 그건 아무도 안 준다.

That's 2–3 weeks of work. Or one line:

2~3주짜리 작업이다. 아니면 한 줄:

app.use(createOneSubMiddleware(config));

How it works

동작 방식

onesub is not a service you sign up for. It is middleware you mount inside the backend you already run. Receipts and webhooks land on your host; nothing routes through us.

onesub은 가입해서 쓰는 서비스가 아니다. 이미 운영 중인 당신의 백엔드에 얹는 미들웨어다. 영수증도 웹훅도 당신의 호스트로 들어온다. 우리 쪽을 경유하는 건 아무것도 없다.

Your app

당신의 앱

react-native-iap · Unity IAP · StoreKit · any client

Apple · Google

StoreKit 2 · Play Billing

receipt / purchaseToken  ↓  server notifications ↓ 영수증 / purchaseToken  ↓  서버 알림 ↓

Your backend — your server, your database

당신의 백엔드 — 당신의 서버, 당신의 DB

app.use(createOneSubMiddleware(config))

POST /onesub/validate · GET /onesub/status · POST /onesub/webhook/apple · POST /onesub/webhook/google · GET /onesub/entitlements

POST /onesub/validate · GET /onesub/status · POST /onesub/webhook/apple · POST /onesub/webhook/google · GET /onesub/entitlements

↓   subscriptions · purchases · entitlements ↓   구독 · 구매 · entitlement

Your PostgreSQL / Redis / in-memory

당신의 PostgreSQL / Redis / 인메모리

Pluggable SubscriptionStore and PurchaseStore — or bring your own.

교체 가능한 SubscriptionStore · PurchaseStore — 직접 구현해 꽂아도 된다.

Webhooks are the fast path, not the only path. Apple's notifications are reliable but not guaranteed, so /onesub/status can fall back to a direct App Store Server API fetch; Google falls back to subscriptionsv2.get. A dropped notification does not leave a user's state drifting.

웹훅은 빠른 경로일 뿐, 유일한 경로가 아니다. Apple 알림은 안정적이지만 보장되진 않는다. 그래서 /onesub/status는 App Store Server API 직접 조회로 폴백하고, Google은 subscriptionsv2.get로 폴백한다. 알림 하나 놓쳤다고 유저 상태가 어긋난 채 남지 않는다.

The whole integration

통합의 전부

Mount the middleware on the server. Ask one endpoint from the client. The React Native SDK is optional sugar on top of that same endpoint.

서버에 미들웨어를 얹는다. 클라이언트에선 엔드포인트 하나를 부른다. React Native SDK는 그 위에 얹는 선택 사항일 뿐이다.

import { createOneSubMiddleware, PostgresSubscriptionStore, PostgresPurchaseStore } 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),
  purchaseStore: new PostgresPurchaseStore(process.env.DATABASE_URL),
}));

Routes mount themselves. The canonical Postgres schema ships with the package at sql/schema.sql, or store.initSchema() applies it on startup.

라우트는 알아서 마운트된다. Postgres 스키마는 패키지에 sql/schema.sql로 동봉되어 있고, store.initSchema()가 부팅 시 대신 적용해주기도 한다.

Lifecycle states, classified — not inferred

라이프사이클 상태 — 추론이 아니라 분류

Most homegrown backends guess state from expiryTime and cancelReason. onesub reads Apple's notification subtype and Google's subscriptionState enum directly, so “your card failed but you still have access” stays a different message from “your subscription is on hold.”

직접 만든 백엔드 대부분은 expiryTime과 cancelReason으로 상태를 추측한다. onesub은 Apple 알림의 subtype과 Google의 subscriptionState enum을 직접 읽는다. 그래야 “카드 결제는 실패했지만 아직 쓸 수 있음”과 “구독 정지됨”이 서로 다른 메시지로 남는다.

State상태 active When언제 UX hintUX 힌트
active✅ paid period정상 결제 기간 normal평소대로
grace_period✅ payment failed, store still grants access결제 실패했지만 스토어가 아직 접근 허용 “Check your payment method — you still have access”결제 정보 확인 필요 (계속 사용 가능)
on_hold❌ grace ended, billing retry continues유예 종료, 결제 재시도 중 “Update your payment method”결제 정보를 업데이트하세요
paused❌ user-voluntary pause (Google only)유저가 자발적으로 일시정지 (Google만) “Resumes on {autoResumeTime}”재개 예정: {autoResumeTime}
expired❌ natural end without renewal갱신 없이 자연 만료 re-purchase재구매
canceled❌ refunded or revoked환불 또는 회수 re-purchase / restore재구매 / 복원

active is computed as (active || grace_period) && expiresAt > now. The expiresAt check is a backstop for a missed EXPIRED webhook and for the 'until_expiry' refund policy.

active는 (active || grace_period) && expiresAt > now로 계산된다. expiresAt 검사는 EXPIRED 웹훅을 놓쳤을 때와 'until_expiry' 환불 정책을 위한 안전망이다.

What's under the hood

내부에서 벌어지는 일

The edge cases that cost weeks the first time you meet them, already handled.

처음 부딪히면 몇 주를 잡아먹는 엣지 케이스들, 이미 처리되어 있다.

Full Apple JWS chain

Apple JWS 전체 체인

StoreKit 2 JWS verified against Apple Root CA G3 through the whole x5c chain — not a decoded payload you hope is real.

StoreKit 2 JWS를 x5c 체인 전체를 따라 Apple Root CA G3까지 검증한다. payload만 디코딩하고 믿는 방식이 아니다.

Google Play v3, not v1

Google Play v3, v1 아님

purchases.subscriptionsv2.get returns a subscriptionState enum that maps straight onto lifecycle states.

purchases.subscriptionsv2.get가 돌려주는 subscriptionState enum이 라이프사이클 상태로 그대로 매핑된다.

The 3-day refund trap

3일 환불 함정

Google auto-refunds anything you don't acknowledgePurchase within 3 days. onesub calls it automatically — for subscriptions and one-time IAP both.

Google은 3일 안에 acknowledgePurchase를 안 하면 자동 환불한다. onesub은 구독·일회성 IAP 양쪽 모두 자동으로 호출한다.

Refund policy you choose

환불 정책 선택

'immediate' flips status to canceled at once; 'until_expiry' keeps entitlement to the original expiry date.

'immediate'는 즉시 canceled로 뒤집고, 'until_expiry'는 원래 만료일까지 권한을 유지한다.

Entitlements over product ids

상품 ID 위의 entitlement

Map premium to any set of subscription and non-consumable products. Consumables are excluded by design — they grant a resource, not an ongoing right.

premium 하나에 구독·비소모성 상품 여러 개를 묶는다. 소모성은 의도적으로 제외된다 — 지속되는 권리가 아니라 일회성 자원이니까.

Built to scale out

스케일 아웃 대비

Redis-backed cache and idempotency, optional BullMQ webhook queue with dead-letter replay, OpenTelemetry spans, generated OpenAPI doc.

Redis 캐시·멱등성, 선택적 BullMQ 웹훅 큐와 dead-letter 재처리, OpenTelemetry 스팬, 자동 생성 OpenAPI 문서.

Multi-app on one server

한 서버에 여러 앱

Isolate credentials for several Apple bundle ids and Google package names. An unknown appId never falls back to another app's keys.

여러 Apple 번들 ID·Google 패키지명의 자격증명을 격리한다. 모르는 appId가 다른 앱 키로 폴백하는 일은 없다.

Apple promotional offers

Apple 프로모션 오퍼

Server-side signing of promotional offer payloads, plus Family Sharing ownership mapping and the CONSUMPTION_REQUEST response hook.

프로모션 오퍼 페이로드 서버 서명, 가족 공유 소유권 매핑, CONSUMPTION_REQUEST 응답 훅까지.

Mock mode for agents

에이전트용 목 모드

npx @onesub/cli dev runs a fully mocked server. Receipt prefixes like MOCK_EXPIRED drive deterministic scenarios with no store credentials.

npx @onesub/cli dev로 완전히 목킹된 서버가 뜬다. MOCK_EXPIRED 같은 영수증 프리픽스로 스토어 자격증명 없이 시나리오를 재현한다.

vs RevenueCat

RevenueCat과 비교

RevenueCat is good software. These are the two axes where a self-hosted answer wins: you own the surface, and you own the data.

RevenueCat은 좋은 제품이다. 셀프 호스팅이 이기는 축은 두 개다 — 인프라를 소유한다는 것, 그리고 데이터를 소유한다는 것.

RevenueCatonesub
Receipt validation영수증 검증 Their servers그쪽 서버 Your server당신의 서버
Revenue share수수료 1% after $2.5K$2.5K 이후 1% 0% forever영구 0%
Data ownership데이터 소유 Their database그쪽 DB Your database당신의 DB
Vendor lock-in벤더 종속 Yes있음 No — MIT open source없음 — MIT 오픈소스
Dashboard대시보드 Hosted호스팅형 Self-hosted, Docker셀프 호스팅, Docker

onesub is not a full RevenueCat replacement. RevenueCat provides hosted infrastructure, experiments, and deeper revenue analytics. onesub provides a self-hosted operational dashboard and core lifecycle metrics for developers who want to own their subscription infrastructure. Already on RevenueCat? There is a step-by-step migration guide covering client code, historical data, webhook switchover, and rollback.

onesub은 RevenueCat의 완전한 대체재가 아니다. RevenueCat은 호스팅 인프라, 실험(A/B), 더 깊은 매출 분석을 제공한다. onesub이 주는 건 셀프 호스팅 운영 대시보드와 핵심 라이프사이클 지표 — 구독 인프라를 직접 소유하고 싶은 개발자를 위한 것이다. 이미 RevenueCat을 쓰고 있다면 클라이언트 코드, 과거 데이터, 웹훅 전환, 롤백까지 다루는 단계별 마이그레이션 가이드가 있다.

Packages

패키지

Take the server alone, or the whole toolkit. Everything except the dashboard image is on npm.

서버만 가져다 써도 되고, 전체 툴킷을 써도 된다. 대시보드 이미지를 뺀 나머지는 전부 npm에 있다.

Package패키지 What역할 Install설치
@onesub/server
latest version on npm
Express middleware — receipt validation + webhooksExpress 미들웨어 — 영수증 검증 + 웹훅 npm i @onesub/server
@jeonghwanko/onesub-sdk
latest version on npm
React Native SDK — useOneSub() + <Paywall />React Native SDK — useOneSub() + <Paywall /> npm i @jeonghwanko/onesub-sdk
@onesub/mcp-server
latest version on npm
MCP tools — AI sets up products and paywallsMCP 도구 — AI가 상품·페이월을 세팅 npx @onesub/mcp-server
@onesub/providers
latest version on npm
App Store Connect + Google Play API wrappers, standaloneApp Store Connect + Google Play API 래퍼, 단독 사용 가능 npm i @onesub/providers
@onesub/cli
latest version on npm
Scaffolds a starter server; dev runs a fully mocked one스타터 서버 스캐폴딩; dev는 완전 목킹 서버 실행 npx @onesub/cli init
onesub-dashboard Self-hosted operations dashboard셀프 호스팅 운영 대시보드 docker run ghcr.io/jeonghwanko/onesub-dashboard
com.onesub.unity Unity purchasing, restore, localized price, server validationUnity 구매·복원·현지화 가격·서버 검증 UPM Git URLUPM Git URL

Own your subscription infrastructure

구독 인프라를 직접 소유하기

MIT licensed. No account, no dashboard signup, no revenue share. Clone it, read it, change it.

MIT 라이선스. 계정도, 대시보드 가입도, 수수료도 없다. 클론하고, 읽고, 고쳐 쓰면 된다.

$ npx @onesub/cli init
Read the source소스 보기