İçeriğe atla

Platformlar Arası Abonelik Yetkilendirmesi: Web, iOS ve Android Senkronizasyonu

EventBridge ve idempotent işleme ile web, iOS ve Android genelinde abonelik erişimini tutarlı tutan güvenilir bir yetkilendirme senkronizasyon katmanı nasıl kurulur.

Ayhan Sipahi Ayhan Sipahi

Bir ürün web, iOS ve Android üzerinde abonelik sattığında, her platform kendi şemasında, kendi yaşam döngüsüyle ve kendi teslim gecikmesiyle abonelik olayları yayar. Yetkilendirme senkronizasyon katmanı, bu heterojen olayları “bu kullanıcı şu anda neye erişebilir?” sorusuna tek ve tutarlı bir cevaba dönüştüren parçadır. Platform başına naif bir handler, saatler içinde senkronizasyondan çıkar; asıl sorun webhook tesisatı değil, dağıtık durumun kendisidir.

Ayakta kalan tasarım dar bir tasarımdır: her sağlayıcı olayını giriş noktasında normalleştirin, bir AWS EventBridge veriyoluna yayınlayın ve uygulamanızın okuduğu DynamoDB yetkilendirme deposunu tek bir idempotent tüketiciye emanet edin. İadeler, aile paylaşımı ve ödemesiz kullanım dönemleri, bu pipeline’ın karşılığını verdiği yerlerdir.

Platformlar Arası Yetkilendirme Sorunu#

Web için Stripe, iOS için Apple App Store, Android için Google Play: bunların her biri kendi formatında, kendi hızında ve kendi yaşam döngüsü modeliyle abonelik olayları gönderir.

Server Notification v2

RTDN Pub/Sub

Webhook Events

Apple App Store

Webhook Girişi

Google Play

Stripe

EventBridge

Yetkilendirme Sync Lambda

Yetkilendirme Deposu

(DynamoDB)

iOS Uygulama

Android Uygulama

Web Uygulama

Her ödeme sağlayıcısının benzer kavramlar için farklı olay adları var. Apple buna DID_RENEW diyor. Stripe invoice.payment_succeeded diyor. Google SUBSCRIPTION_RENEWED diyor. Apple’dan bir yenileme olayı dakikalar sonra gelebilirken, Stripe neredeyse anında tetikleniyor.

Yetkilendirme katmanı olmadan, uygulama kodunuz her yere dağılmış platforma özgü kontrollerle doluyor. Bu yaklaşım, dördüncü bir ödeme kaynağı eklediğinizde veya abonelik katmanlarınızı değiştirdiğinizde bozuluyor.

Yetkilendirme Katmanı Tasarımı#

Yetkilendirme katmanı, ödeme sağlayıcılarınız ile uygulama mantığınız arasında yer alır. Tek bir soruyu yanıtlar: bir kullanıcı ID’si verildiğinde hangi özellikler ve erişim seviyeleri şu anda aktif?

Doğruluk Kaynağı İlkesi#

Erişim kararlarında doğruluk kaynağı yetkilendirme deposudur. Faturalama durumu ödeme sağlayıcılarına aittir; kullanıcının ne yapabileceği ise yetkilendirme deponuza.

Bu ayrım önemli. Bir ödeme sağlayıcısı, kullanıcı hala erişim sahibiyken bir aboneliği “past_due” olarak raporlayabilir. Bu kuralları yetkilendirme katmanınız tanımlar.

Yetkilendirme Tablosu Tasarımı#

Yetkilendirmeler basit boolean değildir. Bir abonelik aktif, ödemesiz kullanım döneminde, duraklatılmış veya faturalama yeniden denemesinde olabilir ve her durum farklı bir erişim seviyesine karşılık gelir.

// Yetkilendirmeler için DynamoDB tablo tasarımı
interface EntitlementRecord {
  // Partition key: kullanıcı kimliğinden türetilir
  pk: string;  // "USER#usr_abc123"
  // Sort key: yetkilendirme tanımlayıcısı
  sk: string;  // "ENT#premium"

  userId: string;
  entitlementId: string;  // "premium", "team", "enterprise"
  status: "active" | "grace_period" | "billing_retry" | "paused" | "expired" | "revoked";
  source: "apple" | "google" | "stripe" | "manual";
  sourceSubscriptionId: string;
  plan: string;  // "monthly_premium", "annual_team"
  features: string[];  // ["unlimited_projects", "api_access", "priority_support"]

  activatedAt: string;  // ISO 8601
  expiresAt: string;  // ISO 8601
  gracePeriodEndsAt?: string;

  lastEventId: string;  // Idempotency takibi için
  lastEventTimestamp: string;
  updatedAt: string;
  ttl?: number;  // Süresi dolan kayıtları otomatik temizler
}

source alanı hangi platformun bu yetkilendirmeyi oluşturduğunu takip eder. Çakışmaları ele alırken bu kritik hale gelir: aynı kullanıcı hem Apple hem de Stripe’dan abone olduğunda, hangi yetkilendirmenin öncelikli olduğunu bilmeniz gerekir.

Özellik Eşleme#

Abonelik planlarını plan adlarına güvenmek yerine somut özelliklere eşleyin. Bu, erişim mantığınızı fiyatlandırma yapınızdan ayırır.

const PLAN_FEATURES: Record<string, string[]> = {
  "free": ["basic_access", "3_projects"],
  "monthly_premium": ["unlimited_projects", "api_access", "export"],
  "annual_premium": ["unlimited_projects", "api_access", "export", "priority_support"],
  "team": ["unlimited_projects", "api_access", "export", "priority_support", "team_management"],
};

Erişim kontrolü yaparken plan adları yerine özellikleri sorgulayın:

async function hasFeature(userId: string, feature: string): Promise<boolean> {
  const entitlements = await getActiveEntitlements(userId);
  return entitlements.some(ent =>
    ent.status === "active" && ent.features.includes(feature)
  );
}

Platformlar Arası Senkronizasyon Stratejileri#

Client’ları yetkilendirme deponuzla senkronize tutmak için üç yaklaşım var.

Polling#

Client periyodik olarak yetkilendirme API’nizi çağırır. Uygulaması basit, ancak gecikme ekler: bir kullanıcı erişim güncellemesini görmeden önce polling aralığı kadar bekleyebilir.

En uygun: gerçek zamanlı erişim güncellemelerinin kritik olmadığı uygulamalar. Tipik polling aralığı: aktif oturumlar için 30-60 saniye, arka plan için 5 dakika.

Push (WebSocket / Push Notification)#

Sunucu, bağlı client’lara WebSocket veya mobil push notification ile yetkilendirme değişikliklerini iletir. Neredeyse anında güncelleme sağlar ancak altyapı karmaşıklığı ekler.

En uygun: anlık erişim değişikliklerinin önemli olduğu uygulamalar (işbirliği araçları, streaming servisleri).

Hibrit (Önerilen)#

Sunucu tarafı webhook işlemeyi client tarafı akıllı polling ile birleştirin. Sunucu webhook’ları işler ve yetkilendirme deposunu hemen günceller. Client’lar düzenli aralıklarla poll yapar, ancak belirli tetikleyicilerde de yeniler.

// Client tarafında akıllı önbellekli yetkilendirme kontrolü
class EntitlementClient {
  private cache: Map<string, { data: EntitlementRecord[]; fetchedAt: number }> = new Map();
  private readonly CACHE_TTL_MS = 30_000; // 30 saniye

  async getEntitlements(userId: string, forceRefresh = false): Promise<EntitlementRecord[]> {
    const cached = this.cache.get(userId);
    const now = Date.now();

    if (!forceRefresh && cached && (now - cached.fetchedAt) < this.CACHE_TTL_MS) {
      return cached.data;
    }

    const response = await fetch(`/api/entitlements/${userId}`);
    const data = await response.json();

    this.cache.set(userId, { data, fetchedAt: now });
    return data;
  }

  // Uygulama ön plana geldiğinde, satın alma sonrası, push bildiriminde çağır
  async refreshEntitlements(userId: string): Promise<EntitlementRecord[]> {
    return this.getEntitlements(userId, true);
  }
}

Zorunlu yenilemeyi tetikleyen durumlar:

  • Uygulama ön plana döndüğünde
  • Kullanıcı bir satın alma akışını tamamladığında
  • Abonelik değişiklikleri hakkında push notification alındığında
  • Kullanıcı premium bir özelliğe gittiğinde

Webhook Güvenilirliği#

Webhook’lar sunucu tarafı yetkilendirme güncellemelerinin omurgasıdır. Ancak webhook’lar doğası gereği güvenilmezdir: sırası bozulmuş gelebilir, tekrarlanabilir veya sessizce başarısız olabilir. Güvenilir bir webhook pipeline’ı dört kalıp gerektirir.

1. Önce Kuyruk, Sonra İşleme#

Webhook’u aldığınız anda HTTP 200 döndürün, işlemeyi asenkron olarak sonraya bırakın. Bu, zaman aşımlarını önler ve ödeme sağlayıcısının gereksiz yere yeniden denemesini engeller.

// Webhook girişi: hızlı onay, asenkron işleme
import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";

const eventBridge = new EventBridgeClient({});

export async function handler(event: APIGatewayProxyEvent) {
  const provider = event.pathParameters?.provider; // "stripe", "apple", "google"
  const body = JSON.parse(event.body || "{}");

  // Adım 1: İmza doğrula (sağlayıcıya özel)
  if (!verifyWebhookSignature(provider, event)) {
    return { statusCode: 401, body: "Invalid signature" };
  }

  // Adım 2: Olayı normalleştir
  const normalized = normalizePaymentEvent(provider, body);

  // Adım 3: Hemen EventBridge'e ilet
  await eventBridge.send(new PutEventsCommand({
    Entries: [{
      Source: "payments.webhook",
      DetailType: `subscription.${normalized.action}`,
      Detail: JSON.stringify(normalized),
      EventBusName: "entitlements",
    }],
  }));

  // Adım 4: Hızlıca 200 dön
  return { statusCode: 200, body: "OK" };
}

2. Olay Normalleştirme#

Her sağlayıcı farklı payload gönderir. Daha fazla işlemden önce bunları ortak bir şemaya normalleştirin.

interface NormalizedSubscriptionEvent {
  eventId: string;  // Benzersiz olay tanımlayıcısı
  action: "created" | "renewed" | "canceled" | "expired" | "refunded" | "grace_period" | "billing_retry";
  userId: string;  // Kendi iç kullanıcı ID'niz
  source: "apple" | "google" | "stripe";
  sourceSubscriptionId: string;
  plan: string;
  currency: string;
  amount: number;  // En küçük birimde (kuruş/cent)
  timestamp: string;  // ISO 8601
  expiresAt: string;
  metadata: Record<string, string>;
}

function normalizePaymentEvent(
  provider: string,
  rawEvent: Record<string, unknown>
): NormalizedSubscriptionEvent {
  switch (provider) {
    case "stripe":
      return normalizeStripeEvent(rawEvent);
    case "apple":
      return normalizeAppleEvent(rawEvent);
    case "google":
      return normalizeGoogleEvent(rawEvent);
    default:
      throw new Error(`Bilinmeyen sağlayıcı: ${provider}`);
  }
}

function normalizeStripeEvent(raw: Record<string, unknown>): NormalizedSubscriptionEvent {
  const event = raw as { id: string; type: string; data: { object: Record<string, unknown> } };
  const sub = event.data.object;

  const ACTION_MAP: Record<string, NormalizedSubscriptionEvent["action"]> = {
    "customer.subscription.created": "created",
    "invoice.payment_succeeded": "renewed",
    "customer.subscription.deleted": "canceled",
    "customer.subscription.updated": "renewed",
    "charge.refunded": "refunded",
  };

  return {
    eventId: event.id,
    action: ACTION_MAP[event.type] || "renewed",
    userId: sub.metadata?.userId as string || "",
    source: "stripe",
    sourceSubscriptionId: sub.id as string,
    plan: (sub.items as { data: { price: { id: string } }[] })?.data?.[0]?.price?.id || "",
    currency: sub.currency as string || "usd",
    amount: sub.amount as number || 0,
    timestamp: new Date().toISOString(),
    expiresAt: new Date((sub.current_period_end as number) * 1000).toISOString(),
    metadata: sub.metadata as Record<string, string> || {},
  };
}

3. Idempotent İşleme#

Webhook’lar en az bir kez teslim edilir, yani aynı olay birden fazla kez gelebilir. Olay ID’sini DynamoDB koşullu yazma ile idempotency anahtarı olarak kullanın.

import { makeIdempotent, IdempotencyConfig } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: "IdempotencyStore",
});

async function processEntitlementEvent(
  event: NormalizedSubscriptionEvent
): Promise<{ updated: boolean }> {
  // Bu olay, işlenen son olaydan daha yeni mi?
  const current = await getEntitlement(event.userId, event.plan);

  if (current && current.lastEventTimestamp >= event.timestamp) {
    // Bayat olay: daha yenisi zaten işlendi
    return { updated: false };
  }

  const statusMap: Record<string, EntitlementRecord["status"]> = {
    created: "active",
    renewed: "active",
    canceled: "expired",
    expired: "expired",
    refunded: "revoked",
    grace_period: "grace_period",
    billing_retry: "billing_retry",
  };

  await updateEntitlement({
    userId: event.userId,
    entitlementId: planToEntitlement(event.plan),
    status: statusMap[event.action] || "active",
    source: event.source,
    sourceSubscriptionId: event.sourceSubscriptionId,
    plan: event.plan,
    features: PLAN_FEATURES[event.plan] || [],
    expiresAt: event.expiresAt,
    lastEventId: event.eventId,
    lastEventTimestamp: event.timestamp,
  });

  return { updated: true };
}

// Powertools idempotency ile sarmala
const idempotencyConfig = new IdempotencyConfig({
  eventKeyJmespath: "detail.eventId",
});

export const handler = makeIdempotent(
  async (event: { detail: NormalizedSubscriptionEvent }) => {
    return processEntitlementEvent(event.detail);
  },
  {
    persistenceStore,
    config: idempotencyConfig,
  }
);

Önemli nokta: hem Powertools idempotency (Lambda seviyesinde tekrar engelleme) hem de zaman damgası karşılaştırması (sırası bozuk gelen olayları ele alma) kullanın. Geç gelen bir olay, daha yeni bir durumun üzerine yazmamalı.

4. Dead Letter Queue (DLQ)#

Tüm yeniden denemelerden sonra işlenemeyen olayların bir yere gitmesi gerekir. DLQ bunları inceleme ve yeniden oynatma için yakalar.

Başarı

Yeniden denemeler sonrası başarısızlık

Alarm

Tekrar Oynat

EventBridge Kuralı

Yetkilendirme Lambda

DynamoDB Güncelleme

SQS DLQ

DLQ İşlemci Lambda

CloudWatch Alarmı

EventBridge Yetkilendirme Mimarisi#

EventBridge, yetkilendirme senkronizasyonu için doğal bir seçimdir: içerik tabanlı yönlendirme, yerleşik yeniden deneme ve Lambda ile doğrudan entegrasyon sunar. Tam pipeline şöyle işliyor.

Olay Veriyolu ve Kurallar#

Yetkilendirme olayları için özel bir olay veriyolu oluşturun. Olayları doğru işleme hedeflerine yönlendirmek için kurallar kullanın.

// Yetkilendirme pipeline'ı için CDK altyapısı
import * as cdk from "aws-cdk-lib";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as lambda from "aws-cdk-lib/aws-lambda-nodejs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as sqs from "aws-cdk-lib/aws-sqs";

export class EntitlementSyncStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string) {
    super(scope, id);

    // Yetkilendirme deposu
    const entitlementTable = new dynamodb.Table(this, "EntitlementTable", {
      partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
      sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      timeToLiveAttribute: "ttl",
    });

    // Idempotency deposu
    const idempotencyTable = new dynamodb.Table(this, "IdempotencyTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      timeToLiveAttribute: "expiration",
    });

    // Başarısız olaylar için DLQ
    const dlq = new sqs.Queue(this, "EntitlementDLQ", {
      retentionPeriod: cdk.Duration.days(14),
    });

    // Özel olay veriyolu
    const bus = new events.EventBus(this, "EntitlementBus", {
      eventBusName: "entitlements",
    });

    // Yetkilendirme senkronizasyon Lambda'sı
    const syncFn = new lambda.NodejsFunction(this, "EntitlementSyncFn", {
      entry: "src/handlers/entitlement-sync.ts",
      environment: {
        ENTITLEMENT_TABLE: entitlementTable.tableName,
        IDEMPOTENCY_TABLE: idempotencyTable.tableName,
      },
      timeout: cdk.Duration.seconds(30),
      retryAttempts: 2,
      deadLetterQueue: dlq,
    });

    entitlementTable.grantReadWriteData(syncFn);
    idempotencyTable.grantReadWriteData(syncFn);

    // Tüm abonelik olaylarını senkronizasyon Lambda'sına yönlendir
    new events.Rule(this, "SubscriptionEventRule", {
      eventBus: bus,
      eventPattern: {
        source: ["payments.webhook"],
        detailType: [
          "subscription.created",
          "subscription.renewed",
          "subscription.canceled",
          "subscription.expired",
          "subscription.refunded",
          "subscription.grace_period",
          "subscription.billing_retry",
        ],
      },
      targets: [new targets.LambdaFunction(syncFn)],
    });
  }
}

Olay Şeması#

Yetkilendirme kararları için gereken tüm bağlamı taşıyan tutarlı bir olay şeması kullanın.

{
  "source": "payments.webhook",
  "detail-type": "subscription.renewed",
  "detail": {
    "eventId": "evt_stripe_abc123",
    "action": "renewed",
    "userId": "usr_def456",
    "source": "stripe",
    "sourceSubscriptionId": "sub_xyz789",
    "plan": "monthly_premium",
    "currency": "usd",
    "amount": 999,
    "timestamp": "2025-04-02T10:30:00Z",
    "expiresAt": "2025-05-02T10:30:00Z",
    "metadata": {
      "originalPlatform": "web"
    }
  }
}

EventBridge, üstel geri çekilme ile yerleşik yeniden deneme sağlar: 24 saat boyunca 185 yeniden denemeye kadar. DLQ ile birlikte bu, size birden fazla başarısızlık koruma katmanı verir.

Çoklu Platform Çakışmaları#

En zorlu uç durum: bir kullanıcı hem iOS’ta (Apple üzerinden) hem de web’de (Stripe üzerinden) premium planınıza abone oluyor. Şimdi farklı kaynaklardan aynı özellik seti için iki aktif yetkilendirmeniz var.

Çakışma Çözüm Stratejisi#

Bir platform öncelik hiyerarşisi tanımlayın. Çakışmalar ortaya çıktığında, daha yüksek öncelikli kaynak erişim kararlarında kazanır, ancak her iki yetkilendirme de takip edilmeye devam eder.

const PLATFORM_PRIORITY: Record<string, number> = {
  manual: 100,  // Yönetici geçersiz kılmaları her zaman kazanır
  stripe: 80,  // Web abonelikleri (en iyi marjınız)
  google: 60,
  apple: 40,  // En büyük platform payını Apple alıyor
};

async function resolveEntitlementConflict(
  userId: string,
  entitlementId: string
): Promise<EntitlementRecord> {
  const entitlements = await getAllEntitlements(userId, entitlementId);
  const active = entitlements.filter(e =>
    e.status === "active" || e.status === "grace_period"
  );

  if (active.length <= 1) {
    return active[0];
  }

  // Birden fazla aktif yetkilendirme: en yüksek önceliklisini seç
  active.sort((a, b) =>
    (PLATFORM_PRIORITY[b.source] || 0) - (PLATFORM_PRIORITY[a.source] || 0)
  );

  return active[0];
}

Warning

Düşük öncelikli aboneliği otomatik olarak iptal etmeyin. Kullanıcıyı yinelenen abonelikleri olduğu konusunda bilgilendirin ve hangisini tutacaklarını kendileri seçsin. Otomatik iptal, destek talepleri ve ücret iadelerine neden olur.

Ödemesiz Kullanım Dönemi Farklılıkları#

Her platform ödemesiz kullanım dönemlerini farklı ele alır:

  • Apple: Yapılandırılabilir faturalama ödemesiz kullanım dönemi 3, 16 veya 28 gün (haftalık abonelikler için 6 gün), ardından Apple’ın ödeme tahsil etmeye çalıştığı 60 günlük faturalama yeniden deneme penceresi
  • Google: Yapılandırılabilir ödemesiz kullanım dönemi (7 veya 30 güne kadar), ardından 60 gün eksi ödemesiz kullanım dönemi süresi olarak hesaplanan hesap askıya alma dönemi
  • Stripe: Yapılandırılabilir dunning davranışı ile Smart Retries üzerinden yapılandırılabilir yeniden deneme planı

Yetkilendirme katmanınızın bu platforma özgü durumları kendi ödemesiz kullanım dönemi mantığınıza eşlemesi gerekir. En güvenli yaklaşım: herhangi bir aktif ödemesiz kullanım döneminde erişimi sürdürün ve tüm yeniden deneme pencereleri kapandıktan sonra yetkilendirmenin süresinin dolmasına izin verin.

Mutabakat#

Idempotent işleme ve DLQ’larla bile sapma olur. Zamanlanmış bir mutabakat görevi, yetkilendirme deponuzu her ödeme sağlayıcısının abonelik API’si ile karşılaştırır ve tutarsızlıkları düzeltir.

// Zamanlanmış mutabakat: her 6 saatte bir çalışır
async function reconcileEntitlements(): Promise<void> {
  const activeEntitlements = await scanActiveEntitlements();

  for (const entitlement of activeEntitlements) {
    const providerState = await fetchProviderSubscription(
      entitlement.source,
      entitlement.sourceSubscriptionId
    );

    if (!providerState) {
      // Abonelik sağlayıcı tarafında artık mevcut değil
      await expireEntitlement(entitlement);
      continue;
    }

    const expectedStatus = mapProviderStatus(entitlement.source, providerState.status);
    if (expectedStatus !== entitlement.status) {
      await updateEntitlementStatus(entitlement, expectedStatus);
      console.log(
        `Mutabakat düzeltmesi: ${entitlement.userId} ${entitlement.entitlementId} ` +
        `${entitlement.status} -> ${expectedStatus}`
      );
    }
  }
}

Sapmayı yakalamak için yeterince sık çalıştırın, ancak sağlayıcı API hız sınırlarına takılmayacak kadar seyrek. Çoğu ürün için her 4-6 saatte bir iyi çalışır. Mutabakat düzeltmelerini her zaman loglayın: çok sayıda görüyorsanız, webhook pipeline’ınızda bir boşluk var.

Temel Çıkarımlar#

  1. Faturalama durumunu erişim durumundan ayırın. Faturalama ödeme sağlayıcılarının, erişim ise yetkilendirme deponuzun sorumluluğundadır. Bu ayrım, ödeme mantığına dokunmadan ödemesiz kullanım dönemlerini, çakışmaları ve promosyonları ele almanıza olanak tanır.

  2. Olayları erken normalleştirin. Sağlayıcıya özgü webhook’ları giriş noktasında ortak bir şemaya dönüştürün. Sonraki her şey tek formatla çalışır.

  3. En az bir kez teslim için oluşturun. Webhook’lar tekrarlanabilir. EventBridge yeniden dener. Bunu güvenli bir şekilde ele almak için idempotency anahtarları ve zaman damgası karşılaştırmaları kullanın.

  4. İlk günden mutabakat ekleyin. Webhook pipeline’ları zamanla sapma gösterir. Periyodik bir mutabakat görevi, kullanıcılar fark etmeden sorunları yakalar.

  5. Yinelenen abonelikleri asla otomatik iptal etmeyin. Bir kullanıcının birden fazla platformda aboneliği olduğunda, bilgilendirin ve kendilerinin karar vermesine izin verin. Yanlışlıkla yapılan iptallerin destek maliyeti, yinelenenler ile başa çıkmanın mühendislik maliyetini çok aşar.

Bu tasarım, erişim kurallarının sahibi sizken geçerlidir: birden fazla faturalama sağlayıcısı, fiyat tablonuzdan uzun ömürlü özellik kapıları ve tek bir yere bakması gereken bir destek ekibi. Bunların altında kalıyorsanız bu katmana gerek yok; tek sağlayıcı ve tek planla çalışan bir üründe abonelik nesnesini istek anında okuyup bir dakika önbelleğe almak yeterlidir. Platformlar arası makbuz yönetimi sizin için yetkilendirme şemasına sahip olmaktan daha önemliyse, RevenueCat gibi yönetilen bir katman aynı işi daha az altyapıyla görür.

Bu mimariyi besleyen ödeme sağlayıcı seçimi için bkz. Ödeme Sağlayıcıları ve Uyumluluk. Burada tüketilen Apple ve Google olaylarını üreten mobil makbuz doğrulaması için bkz. Mobil IAP ve Paywall Stratejileri. Bu yetkilendirme değişikliklerini tetikleyen abonelik yaşam döngüsü yönetimi için bkz. Abonelik Yaşam Döngüsü Yönetimi.

Kaynaklar#

İlgili yazılar