toeverything/AFFiNE · error · ManagedByAppStoreOrPlay
managed_by_app_store_or_play
managed_by_app_store_or_play
Error message
This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.
What it means
Thrown by cancelSubscription when the active subscription's provider is 'revenuecat'. Subscriptions purchased via in-app purchase (App Store / Google Play) are managed read-only by RevenueCat; Stripe-side mutations like cancel are refused so the web backend never fights the store.
Source
Thrown at packages/backend/server/src/plugins/payment/service.ts:173
);
}
async cancelSubscription(
identity: z.infer<typeof SubscriptionIdentity>,
idempotencyKey?: string
): Promise<Subscription> {
this.assertSubscriptionIdentity(identity);
const manager = this.select(identity.plan);
const subscription = await manager.getActiveSubscription(identity);
if (!subscription) {
throw new SubscriptionNotExists({ plan: identity.plan });
}
// IAP read-only: RevenueCat-managed subscriptions cannot be modified on web
if (subscription.provider === 'revenuecat') {
throw new ManagedByAppStoreOrPlay();
}
if (!subscription.stripeSubscriptionId) {
throw new CantUpdateOnetimePaymentSubscription(
'Onetime payment subscription cannot be canceled.'
);
}
if (subscription.canceledAt) {
throw new SubscriptionHasBeenCanceled();
}
// update the subscription in db optimistically
const newSubscription = manager.cancelSubscription(subscription);
// should release the schedule first
if (subscription.stripeScheduleId) {
const manager = await this.scheduleManager.fromSchedule(View on GitHub (pinned to b4c8548c09)
Solutions
- Cancel from the originating store: iOS App Store subscriptions settings or Google Play subscriptions, which flow back through RevenueCat webhooks.
- In the web UI, detect provider === 'revenuecat' on the subscription row and link to the store's manage page instead of the cancel button.
- For automated flows, branch on subscription.provider before attempting web-side cancel/resume.
Example fix
// before
if (sub.status === 'active') await svc.cancelSubscription(identity);
// after
if (sub.provider === 'revenuecat') {
openStoreManagePage(sub);
} else if (sub.status === 'active') {
await svc.cancelSubscription(identity);
} Defensive patterns
Strategy: validation
Validate before calling
if (subscription.provider === 'revenuecat') {
openManageInStore(subscription); // App Store / Google Play link
} else {
await svc.cancelSubscription(identity, idempotencyKey);
} Type guard
function isStoreManaged(s: { provider: string | null }): boolean {
return s.provider === 'revenuecat';
} Try / catch
catch (e) { if (gqlErrorCode(e) === 'managed_by_app_store_or_play') { openManageInStore(subscription); return; } throw e; } Prevention
- Branch all web subscription actions on provider before calling cancel/resume.
- Badge store-managed subscriptions in the UI with a Manage in store link.
- Keep RevenueCat webhook sync healthy so provider is never missing on rows.
When it happens
Trigger: User bought Pro/AI in the iOS or Android app, then opens the web app and clicks Cancel Subscription; a shared account has a mobile-originated subscription and the web client calls cancelSubscription.
Common situations: Cross-platform product where mobile IAP and web Stripe checkout coexist; support macros that always cancel via the web API; webhooks out of sync so web UI does not badge the subscription as store-managed.
Related errors
- invalid_subscription_parameters
- subscription_not_exists
- cant_update_onetime_payment_subscription
- subscription_has_been_canceled
- workspace_id_required_to_update_team_subscription
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/b079eb86d274cbae.
Report an issue: GitHub.