Building subscription functionality in a fitness app development requires more than integrating Apple StoreKit or Google Play Billing.
A successful payment doesn't automatically mean a user should receive permanent premium access. Subscriptions can expire, renew, enter a billing grace period, or be revoked. Users may also switch devices, reinstall the app, or restore earlier purchases.
For example, imagine a fitness app with three subscription tiers:
| Plan | Features |
|---|---|
| Free | Workout tracking and basic progress history |
| Premium | Personalized workout plans and advanced analytics |
| Pro | All Premium features plus personalized coaching |
When someone purchases Premium on an iPhone and later signs in on Android, the backend should determine whether they can access their paid features.
That's where purchase validation, subscription state management, and entitlements become essential.
In this tutorial, we'll design a subscription architecture using:
- TypeScript and Node.js
- PostgreSQL
- Apple StoreKit and App Store Server API
- Google Play Billing and Google Play Developer API
- Server notifications and background workers
- Centralized entitlement authorization
The examples use application-defined interfaces to demonstrate architecture. Store API adapters, authentication middleware, database transactions, and cryptographic verification must be implemented using official platform documentation before production deployment.
1. Understanding Purchases, Subscriptions, and Entitlements
First, separate these concepts.
| Concept | Responsibility |
|---|---|
| Purchase | A transaction initiated through Apple or Google Play |
| Subscription | An ongoing billing relationship |
| Purchase validation | Verifies the authenticity and current state of a purchase |
| Subscription status | Describes renewal, expiration, grace, or revocation |
| Entitlement | Represents permission to access a feature |
| Authorization | Enforces access decisions in the backend |
An entitlement is not the same as a subscription product.
For example, Apple and Google may use different product identifiers, while the fitness app maps both to the same internal premium_training_plans entitlement.
Define Fitness App Entitlements
type Entitlement =
| "basic_workouts"
| "premium_training_plans"
| "advanced_analytics"
| "personalized_coaching";
type Plan = "free" | "premium" | "pro";
const PLAN_FEATURES: Record<Plan, Entitlement[]> = {
free: [
"basic_workouts"
],
premium: [
"basic_workouts",
"premium_training_plans",
"advanced_analytics"
],
pro: [
"basic_workouts",
"premium_training_plans",
"advanced_analytics",
"personalized_coaching"
]
};
This approach makes the subscription system independent of store-specific product identifiers.
Important: Never grant premium access solely through a frontend variable such as:
const isPremium = true;
Client-side information can become outdated or be manipulated.
Protected backend operations must use trusted, server-side entitlement checks.
2. Designing the Subscription Architecture
A reliable subscription system needs several connected components:
- Mobile application
- Apple App Store or Google Play
- Backend subscription API
- Purchase verification service
- Subscription database
- Entitlement service
- Server notification processor
- Reconciliation and monitoring workers
Architecture Diagram
flowchart TD
A["iOS / Android App"]
B["Apple App Store / Google Play"]
C["Backend Subscription API"]
D["Purchase Verification"]
E[("PostgreSQL Database")]
F["Entitlement Service"]
G["Premium Fitness APIs"]
H["Server Notifications"]
I["Queue / Worker"]
J["Reconciliation"]
A -->|"Start purchase"| B
A -->|"Submit purchase reference"| C
C --> D
D <-->|"Verify transaction"| B
D --> E
E --> F
F --> G
G --> A
B --> H
H --> I
I --> D
J --> D
Purchase Workflow
1. User selects a subscription plan.
2. Store processes the purchase.
3. Mobile app receives purchase information.
4. App submits the purchase reference to the backend.
5. Backend verifies information with Apple or Google.
6. Backend checks application and product identity.
7. Verified subscription state is saved.
8. Entitlements are recalculated.
9. Premium APIs authorize the user.
The mobile app initiates purchases, but the backend maintains the access state.
3. Apple and Google Play Purchase Verification
Apple and Google use different identifiers, billing models, and verification workflows.
Apple App Store
Apple provides StoreKit for in-app purchases and the App Store Server API for retrieving server-side transaction and subscription information.
Important identifiers include:
- Transaction ID
- Original transaction ID
- Product ID
- App account token, where used
The App Store Server API supports retrieving subscription statuses and signed transaction information.
Developers must verify relevant signed data, validate application identity, and handle the subscription lifecycle.
Official documentation:
Google Play
Google Play Billing handles supported Android purchase flows.
Important concepts include:
- Product IDs
- Base plans and offers
- Purchase tokens
- Purchase acknowledgement
- Subscription lifecycle states
The backend can retrieve subscription information using the Google Play Developer API's purchases.subscriptionsv2.get method.
Completed purchases must be acknowledged according to Google's requirements. Pending purchases must not prematurely grant paid access.
Official documentation:
4. Implementing Server-Side Purchase Validation
A purchase callback from the mobile application should trigger backend verification.
It should not independently grant premium access.
Step 1: Define Verification Types
type Platform = "apple" | "google";
type AccessState =
| "active"
| "grace"
| "inactive"
| "revoked";
interface VerifiedPurchase {
platform: Platform;
appId: string;
productId: string;
purchaseKey: string;
transactionKey: string;
accessState: AccessState;
accessUntil: Date | null;
verifiedAt: Date;
}
interface PurchaseVerifier {
verify(
purchaseReference: string
): Promise<VerifiedPurchase>;
}
These interfaces normalize verified store information into an application-specific representation.
Apple and Google adapters must implement verification using their respective official mechanisms.
Step 2: Build the Subscription Service
interface SubscriptionRepository {
applyVerifiedPurchaseAtomically(
userId: string,
purchase: VerifiedPurchase
): Promise<void>;
}
class SubscriptionService {
constructor(
private readonly verifiers:
Record<Platform, PurchaseVerifier>,
private readonly subscriptions:
SubscriptionRepository
) {}
async validate(
userId: string,
platform: Platform,
purchaseReference: string
) {
const purchase =
await this.verifiers[platform].verify(
purchaseReference
);
if (purchase.platform !== platform) {
throw new Error("PLATFORM_MISMATCH");
}
if (
purchase.appId !== EXPECTED_APP_IDS[platform]
) {
throw new Error("INVALID_APPLICATION");
}
if (
!ALLOWED_PRODUCT_IDS.has(purchase.productId)
) {
throw new Error("INVALID_PRODUCT");
}
await this.subscriptions
.applyVerifiedPurchaseAtomically(
userId,
purchase
);
return {
status: "verified"
};
}
}
The repository implementation must atomically:
- Enforce purchase ownership.
- Save verified subscription information.
- Recalculate affected entitlements.
- Prevent duplicate or stale updates.
A transaction already associated with one account must not be silently assigned to another account.
Step 3: Create the Validation API
The following is an application-defined endpoint, not a store API.
POST /api/subscriptions/validate
Authorization: Bearer <ACCESS_TOKEN>
Content-Type: application/json
Request body:
{
"platform": "apple",
"purchaseReference": "<TRANSACTION_REFERENCE>"
}
Example response:
{
"status": "verified"
}
The backend must obtain the user ID from authenticated session information, not from an untrusted request field.
Additional production controls include input validation, rate limiting, platform-specific environment checks, and secure error handling.
5. Building a Centralized Entitlement Service
After validating a subscription, the system must determine which fitness features the user can access.
Define Subscription Access
interface SubscriptionAccess {
plan: Plan;
state: AccessState;
accessUntil: Date | null;
}
Evaluate Entitlements
function resolveEntitlements(
subscription: SubscriptionAccess | null,
now = new Date()
): Set<Entitlement> {
const allowed = new Set<Entitlement>(
PLAN_FEATURES.free
);
if (!subscription) {
return allowed;
}
const withinAccessPeriod =
subscription.accessUntil !== null &&
subscription.accessUntil.getTime() > now.getTime();
const validState =
subscription.state === "active" ||
subscription.state === "grace";
if (!validState || !withinAccessPeriod) {
return allowed;
}
for (
const feature of PLAN_FEATURES[subscription.plan]
) {
allowed.add(feature);
}
return allowed;
}
The grace state is an internal abstraction. It should only be assigned when the verified platform state permits continued access.
This sample assumes paid entitlements have an expiration timestamp.
Protect Premium API Resources
async function requireEntitlement(
userId: string,
entitlement: Entitlement
): Promise<void> {
const subscription =
await subscriptionRepository
.getCurrentAccess(userId);
const allowed =
resolveEntitlements(subscription);
if (!allowed.has(entitlement)) {
throw new Error("ENTITLEMENT_REQUIRED");
}
}
Use it before retrieving premium resources.
async function getPremiumWorkoutPlan(
authenticatedUserId: string
) {
await requireEntitlement(
authenticatedUserId,
"premium_training_plans"
);
return workoutRepository.findForUser(
authenticatedUserId
);
}
A production API should translate authorization errors into appropriate HTTP responses, such as 403 Forbidden.
Engineering rule: Hiding a premium button in the mobile UI is not a substitute for backend authorization.
6. Subscription Database Design
The subscription database should track purchases separately from user identities.
Below is a simplified PostgreSQL schema.
CREATE TABLE users (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ
NOT NULL DEFAULT NOW()
);
CREATE TABLE subscription_products (
id UUID PRIMARY KEY,
platform TEXT NOT NULL,
store_product_id TEXT NOT NULL,
internal_plan TEXT NOT NULL,
UNIQUE(platform, store_product_id)
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL
REFERENCES users(id),
platform TEXT NOT NULL,
store_purchase_key TEXT NOT NULL,
product_id UUID NOT NULL
REFERENCES subscription_products(id),
access_state TEXT NOT NULL,
access_until TIMESTAMPTZ,
verified_at TIMESTAMPTZ NOT NULL,
UNIQUE(platform, store_purchase_key)
);
CREATE TABLE purchase_transactions (
id UUID PRIMARY KEY,
subscription_id UUID NOT NULL
REFERENCES subscriptions(id),
transaction_key TEXT NOT NULL,
purchased_at TIMESTAMPTZ,
UNIQUE(subscription_id, transaction_key)
);
CREATE TABLE processed_notifications (
id UUID PRIMARY KEY,
platform TEXT NOT NULL,
event_key TEXT NOT NULL,
processing_status TEXT NOT NULL,
received_at TIMESTAMPTZ
NOT NULL DEFAULT NOW(),
UNIQUE(platform, event_key)
);
This schema is educational rather than production-ready.
Production implementations may additionally require:
- Subscription lineage and linked purchase-token history
- Environment identifiers
- Refund and revocation information
- Notification retry metadata
- Reconciliation audit history
- Transaction isolation and concurrency safeguards
- Explicit entitlement-grant records
Why Unique Constraints Matter
Repeated notifications or concurrent purchase requests must not create duplicate transactions.
Database uniqueness constraints help enforce idempotency, but application logic must also handle transaction conflicts safely.
7. Processing Webhooks and Subscription Notifications
Subscription status can change without any activity inside the fitness application.
Examples include:
- Successful renewals
- Cancellation
- Expiration
- Failed payments
- Grace periods
- Refunds and revocations
- Billing recovery
Apple Notifications
Apple App Store Server Notifications V2 provides signed subscription event information.
The backend must verify relevant signed payloads and interpret the event using Apple's documented behavior.
Google Play Notifications
Google Play uses Real-time Developer Notifications (RTDN), delivered through Google Cloud Pub/Sub.
After receiving an authenticated RTDN, the backend should query the Google Play Developer API to determine the current purchase state.
Safe Notification Workflow
flowchart TD
A["Store Notification"]
B["Verify Authenticity"]
C{"Valid?"}
D["Reject"]
E["Persist Event and Queue"]
F{"Already Recorded?"}
G["Acknowledge Duplicate"]
H["Fetch Verified Store State"]
I["Update Subscription Atomically"]
J["Recalculate Entitlements"]
K["Mark Processed"]
L["Retry / Reconcile"]
A --> B
B --> C
C -->|No| D
C -->|Yes| E
E --> F
F -->|Yes| G
F -->|No| H
H -->|Success| I
H -->|Transient Failure| L
I --> J
J --> K
L --> H
TypeScript Notification Pseudocode
interface VerifiedNotification {
platform: Platform;
eventKey: string;
purchaseReference: string;
}
async function receiveNotification(
platform: Platform,
rawRequest: unknown
) {
// Implement using official platform authentication
// and signature verification requirements.
const event =
await notificationVerifier.verify(
platform,
rawRequest
);
// This operation must atomically persist the event
// and schedule durable processing.
const inserted =
await notificationInbox.insertUniqueAndQueue(
event
);
return {
accepted: true,
duplicate: !inserted
};
}
async function processNotification(
eventKey: string
) {
const event =
await notificationInbox.get(eventKey);
const purchase =
await platformVerifiers[event.platform]
.verify(event.purchaseReference);
// Safely save verified store state, resolve
// account ownership, and update entitlements.
await subscriptionRepository
.applyNotificationAtomically(
event,
purchase
);
}
These functions are application-defined pseudocode, not official SDK methods.
The notification transport, verification mechanisms, and acknowledgements are platform-specific.
Why Direct Event Updates Are Dangerous
Suppose the backend receives a renewal notification followed by an older cancellation notification.
If it blindly applies each event as the current status, it may overwrite the newer subscription state.
Instead, treat notifications as reconciliation triggers.
Retrieve current verified platform information before calculating access.
8. Handling Subscription Lifecycle Edge Cases
Real-world subscriptions involve more than successful initial payments.
| Scenario | Recommended handling |
|---|---|
| Cancellation | Preserve access through the applicable paid period |
| Renewal failure | Check current verified billing state |
| Grace period | Continue access where permitted |
| Account hold | Restrict premium access appropriately |
| Expiration | Remove expired paid entitlements |
| Refund | Recalculate entitlement based on verified refund information |
| Revocation | Remove affected access as required |
| Reinstallation | Restore verified purchase and account access |
| Device change | Synchronize account entitlements |
| Duplicate webhook | Process idempotently |
| Store API outage | Retry safely without inventing access |
| Subscription upgrade | Apply platform-specific replacement rules |
| Wrong account ownership | Require controlled purchase recovery |
Example: Cancellation Before Expiration
A user cancels their subscription on October 10, but their paid period ends on October 25.
The backend should not automatically remove premium access on October 10.
It should preserve access through the verified entitlement period, unless a refund or other revocation changes the entitlement.
Example: Failed Renewal
If a payment fails, the system must evaluate the platform's actual billing state.
Google Play and Apple support billing recovery behavior, but their exact statuses and configurations differ.
Do not assume that every failed renewal immediately ends access.
Example: Store API Downtime
When store verification is temporarily unavailable:
- Record the failure.
- Retry with bounded exponential backoff.
- Preserve appropriate previously verified information.
- Avoid granting new permanent access without validation.
- Schedule reconciliation.
9. Subscription Security and Reliability
Subscription systems control paid functionality and interact with external billing platforms.
They require secure engineering and operational monitoring.
Security Checklist
- [ ] Authenticate subscription API requests.
- [ ] Validate store purchases on the backend.
- [ ] Enforce purchase ownership.
- [ ] Protect premium APIs using entitlements.
- [ ] Verify notification authenticity.
- [ ] Store secrets outside mobile applications.
- [ ] Use least-privilege service credentials.
- [ ] Prevent duplicate event processing.
- [ ] Rate-limit purchase verification requests.
- [ ] Avoid logging sensitive purchase tokens unnecessarily.
- [ ] Monitor reconciliation and notification failures.
- [ ] Implement durable retries and dead-letter handling.
Configuration Example
# Placeholder values only.
DATABASE_URL="<POSTGRES_CONNECTION_STRING>"
APPLE_BUNDLE_ID="<IOS_BUNDLE_ID>"
APPLE_ISSUER_ID="<APP_STORE_ISSUER_ID>"
APPLE_KEY_ID="<APP_STORE_KEY_ID>"
APPLE_PRIVATE_KEY="<SECURE_SECRET_REFERENCE>"
GOOGLE_PLAY_PACKAGE_NAME="<ANDROID_PACKAGE_NAME>"
GOOGLE_SERVICE_ACCOUNT_CREDENTIALS="<SECURE_SECRET_REFERENCE>"
Do not commit real secrets to Git repositories.
Production systems should use appropriate secrets management, logging controls, and credential rotation processes.
If subscription accounts are connected to health-related fitness profiles, consider applicable privacy obligations separately. A secure billing implementation alone does not establish legal compliance.
10. Testing Subscription Workflows
Use Apple's supported StoreKit testing and sandbox environments and Google's supported Play Billing test environments.
Test both client behavior and server-side subscription state.
Important Test Scenarios
| Test | Expected outcome |
|---|---|
| Successful purchase | Premium entitlement activated after verification |
| Invalid reference | Access denied |
| Pending purchase | No premature paid entitlement |
| Renewal | Access period updated correctly |
| Cancellation | Access continues through valid period |
| Expiration | Paid entitlement removed |
| Refund or revocation | Access recalculated |
| Duplicate notification | No duplicate effects |
| Delayed notification | Reconciliation restores accurate state |
| Reinstallation | Verified purchase restored |
| Backend outage | Safe retry and recovery |
| Account conflict | Unauthorized claim prevented |
Unit Testing Entitlements
Using Vitest:
import { describe, expect, it } from "vitest";
describe("subscription entitlements", () => {
it("grants Premium features during valid access", () => {
const entitlements = resolveEntitlements(
{
plan: "premium",
state: "active",
accessUntil:
new Date("2027-01-01T00:00:00Z")
},
new Date("2026-12-01T00:00:00Z")
);
expect(
entitlements.has("premium_training_plans")
).toBe(true);
});
it("denies Premium features after expiration", () => {
const entitlements = resolveEntitlements(
{
plan: "premium",
state: "active",
accessUntil:
new Date("2026-01-01T00:00:00Z")
},
new Date("2026-12-01T00:00:00Z")
);
expect(
entitlements.has("premium_training_plans")
).toBe(false);
});
it("denies revoked subscription access", () => {
const entitlements = resolveEntitlements({
plan: "pro",
state: "revoked",
accessUntil:
new Date("2027-01-01T00:00:00Z")
});
expect(
entitlements.has("personalized_coaching")
).toBe(false);
});
});
These tests validate entitlement logic, not platform purchase verification.
Before release, additionally test signed-data verification, purchase acknowledgement, sandbox configuration, concurrent updates, and account restoration.
11. Common Subscription Implementation Mistakes
1. Trusting Frontend Subscription State
Problem: A modified client can claim premium access.
Solution: Authorize protected operations against backend entitlements.
2. Treating Purchase Callbacks as Final Verification
Problem: Client callbacks do not establish long-term subscription validity.
Solution: Verify purchases through trusted Apple or Google mechanisms.
3. Removing Access Immediately After Cancellation
Problem: Users may lose features during an already-paid period.
Solution: Separate renewal cancellation from entitlement expiration.
4. Ignoring Duplicate Notifications
Problem: Duplicate processing may corrupt subscription records.
Solution: Use unique event identifiers and idempotent database updates.
5. Ignoring Refunds and Revocations
Problem: Premium features may remain available after access is revoked.
Solution: Reconcile authoritative store information.
6. Skipping Subscription Reconciliation
Problem: Missed notifications leave entitlements outdated.
Solution: Implement scheduled reconciliation and failure monitoring.
7. Mixing Store IDs With User IDs
Problem: Account ownership becomes difficult to manage across devices.
Solution: Keep internal application identities separate from store identifiers.
8. Exposing Credentials in Mobile Applications
Problem: Backend credentials can be extracted from distributed applications.
Solution: Store sensitive credentials only in trusted server infrastructure.
12. Production Implementation Roadmap
Build the subscription service in manageable phases.
| Step | Deliverable |
|---|---|
| 1. Define plans | Subscription-to-entitlement mapping |
| 2. Configure products | Apple and Google subscription configuration |
| 3. Design database | Purchase and subscription persistence |
| 4. Implement verification | Platform-specific verification adapters |
| 5. Implement entitlements | Centralized authorization service |
| 6. Integrate notifications | Verified event ingestion |
| 7. Add reconciliation | Reliable background processing |
| 8. Implement restore purchases | Secure account recovery |
| 9. Test lifecycle behavior | Automated and platform-specific tests |
| 10. Deploy monitoring | Alerts, logs, and operational procedures |
Final Engineering Checklist
- [ ] Purchase validation is server-side.
- [ ] Product and application identifiers are verified.
- [ ] Store purchases are associated with the correct account.
- [ ] Entitlements are calculated centrally.
- [ ] Premium API endpoints enforce authorization.
- [ ] Subscription updates are transactionally safe.
- [ ] Webhook processing is idempotent.
- [ ] Missed notifications can be reconciled.
- [ ] Refunds and revocations are handled.
- [ ] Store API credentials remain secure.
- [ ] Subscription restoration works across supported devices.
- [ ] Monitoring identifies failures before they affect many users.
Conclusion
Implementing fitness app subscriptions is primarily a state-management and authorization challenge, not just a payment integration task.
Apple and Google Play provide purchase infrastructure, but application developers must maintain accurate subscription records and feature entitlements.
A reliable implementation requires:
- Server-side purchase validation to establish trusted purchase information.
- Centralized entitlements to determine premium feature access.
- Idempotent notification processing to handle renewals, cancellations, and billing changes.
- Subscription reconciliation to recover from missed events and outdated state.
- Backend authorization to protect premium training plans and other paid features.
The core principle is simple:
Let the store verify purchases, let the backend determine entitlements, and never trust the mobile client as the sole authority for premium access.
That separation makes fitness subscription systems easier to secure, maintain, test, and extend.
For developers: How are you handling subscription recovery and entitlement synchronization when users switch between iOS and Android?
Top comments (0)