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

  1. Cancel from the originating store: iOS App Store subscriptions settings or Google Play subscriptions, which flow back through RevenueCat webhooks.
  2. In the web UI, detect provider === 'revenuecat' on the subscription row and link to the store's manage page instead of the cancel button.
  3. 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

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


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/b079eb86d274cbae. Report an issue: GitHub.