Skip to content
~/sph.sh

Omnichannel Yetkilendirme Senkronizasyonu: Platformlar Arası Abonelik Erişimi

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

Özet

Bir kullanıcı web sitenizden Stripe ile abone oluyor. Beş dakika sonra iOS uygulamanızı açıyor ve kilitli bir ekran görüyor. Bu, omnichannel yetkilendirme problemi -- ve birden fazla platformda abonelik satan her ürünü etkiliyor. Bu yazı, web, iOS ve Android genelinde erişimi tutarlı tutan bir yetkilendirme senkronizasyon katmanının nasıl tasarlanacağını kapsıyor. Farklı ödeme sağlayıcılarından gelen olayları normalleştirmeyi, idempotent webhook handler'lar oluşturmayı, olay tabanlı işleme için EventBridge kullanmayı ve platformlar arası abonelikleri zorlaştıran uç durumları ele almayı öğreneceksiniz.

Platformlar Arası Yetkilendirme Problemi

Ürününüz birden fazla kaynaktan ödeme kabul ettiğinde -- web için Stripe, iOS için Apple App Store, Android için Google Play -- her platform kendi formatında, kendi hızında ve kendi yaşam döngüsü modeliyle abonelik olayları gönderir.

Zorluk bu olayları almak değil. Zorluk, bunları tek ve tutarlı bir cevaba dönüştürmek: "Bu kullanıcı şu anda neye erişebilir?"

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: verilen bir kullanıcı ID'si için, hangi özellikler ve erişim seviyeleri şu anda aktif?

Doğruluk Kaynağı Prensibi

Yetkilendirme deposu -- ödeme sağlayıcısı değil -- erişim kararları için doğruluk kaynağıdır. Ödeme sağlayıcıları faturalama durumu için doğruluk kaynağıdır, ancak yetkilendirme deponuz kullanıcının ne yapabileceğinin doğruluk kaynağıdır.

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

Yetkilendirme Tablo 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ı erişim seviyelerine karşılık gelir.

typescript
// Yetkilendirmeler icin DynamoDB tablo tasarimiinterface EntitlementRecord {  pk: string;              // "USER#usr_abc123"  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;  features: string[];
  activatedAt: string;  expiresAt: string;  gracePeriodEndsAt?: string;
  lastEventId: string;  lastEventTimestamp: string;  updatedAt: string;  ttl?: number;}

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.

typescript
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:

typescript
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.

typescript
// Client tarafinda akilli onbellekli yetkilendirme kontroluclass 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;  }
  async refreshEntitlements(userId: string): Promise<EntitlementRecord[]> {    return this.getEntitlements(userId, true);  }}

Zorunlu yenileme için anahtar tetikleyiciler:

  • 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ıra dışı gelebilir, tekrarlanabilir veya sessizce başarısız olabilir. Güvenilir bir webhook pipeline'ı dört kalıp gerektirir.

1. Önce Kuyruk İşleme

Webhook'u aldığınızda hemen HTTP 200 döndürün. Sonra asenkron olarak işleyin. Bu, zaman aşımlarını önler ve ödeme sağlayıcısının gereksiz yere yeniden denemesini engeller.

typescript
// Webhook giris -- hizli onay, asenkron islemeimport { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";
const eventBridge = new EventBridgeClient({});
export async function handler(event: APIGatewayProxyEvent) {  const provider = event.pathParameters?.provider;  const body = JSON.parse(event.body || "{}");
  // Adim 1: Imza dogrula (saglayiciya ozel)  if (!verifyWebhookSignature(provider, event)) {    return { statusCode: 401, body: "Invalid signature" };  }
  // Adim 2: Olayi normalestir  const normalized = normalizePaymentEvent(provider, body);
  // Adim 3: Hemen EventBridge'e ilet  await eventBridge.send(new PutEventsCommand({    Entries: [{      Source: "payments.webhook",      DetailType: `subscription.${normalized.action}`,      Detail: JSON.stringify(normalized),      EventBusName: "entitlements",    }],  }));
  // Adim 4: Hizlica 200 don  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.

typescript
interface NormalizedSubscriptionEvent {  eventId: string;  action: "created" | "renewed" | "canceled" | "expired" | "refunded" | "grace_period" | "billing_retry";  userId: string;  source: "apple" | "google" | "stripe";  sourceSubscriptionId: string;  plan: string;  currency: string;  amount: number;  timestamp: string;  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 saglayici: ${provider}`);  }}

3. Idempotent İşleme

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

typescript
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 }> {  const current = await getEntitlement(event.userId, event.plan);
  if (current && current.lastEventTimestamp >= event.timestamp) {    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 };}
const idempotencyConfig = new IdempotencyConfig({  eventKeyJmespath: "detail.eventId",});
export const handler = makeIdempotent(  async (event: { detail: NormalizedSubscriptionEvent }) => {    return processEntitlementEvent(event.detail);  },  {    persistenceStore,    config: idempotencyConfig,  });

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

4. Ölü Mektup Kuyruğu (DLQ)

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

EventBridge Yetkilendirme Mimarisi

EventBridge, yetkilendirme senkronizasyonu için doğal bir uyum sağlar çünkü içerik tabanlı yönlendirme, yerleşik yeniden deneme ve doğal Lambda entegrasyonu sunar. İşte tam pipeline.

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.

typescript
// Yetkilendirme pipeline'i icin CDK altyapisiimport * 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);
    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",    });
    const idempotencyTable = new dynamodb.Table(this, "IdempotencyTable", {      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,      timeToLiveAttribute: "expiration",    });
    const dlq = new sqs.Queue(this, "EntitlementDLQ", {      retentionPeriod: cdk.Duration.days(14),    });
    const bus = new events.EventBus(this, "EntitlementBus", {      eventBusName: "entitlements",    });
    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);
    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)],    });  }}

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ını Ele Alma

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.

typescript
const PLATFORM_PRIORITY: Record<string, number> = {  manual: 100,   // Admin gecersiz kilmalar her zaman kazanir  stripe: 80,    // Web abonelikleri (en iyi marjiniz)  google: 60,  apple: 40,     // Apple en yuksek gelir payina sahip};
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];  }
  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.

Uzlaştırma

Idempotent işleme ve DLQ'larla bile sapma olur. Zamanlanmış bir uzlaştırma 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.

typescript
// Zamanlanmis uzlastirma -- her 6 saatte bir calisirasync function reconcileEntitlements(): Promise<void> {  const activeEntitlements = await scanActiveEntitlements();
  for (const entitlement of activeEntitlements) {    const providerState = await fetchProviderSubscription(      entitlement.source,      entitlement.sourceSubscriptionId    );
    if (!providerState) {      await expireEntitlement(entitlement);      continue;    }
    const expectedStatus = mapProviderStatus(entitlement.source, providerState.status);    if (expectedStatus !== entitlement.status) {      await updateEntitlementStatus(entitlement, expectedStatus);      console.log(        `Uzlastirma duzeltmesi: ${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. Uzlaştırma düzeltmelerini her zaman logla -- ç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. Ödeme sağlayıcıları faturalamaya sahiptir. Yetkilendirme deponuz erişime sahiptir. 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 uzlaştırma ekleyin. Webhook pipeline'ları zamanla sapma gösterir. Periyodik bir uzlaştırma 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.

Bir abonelik ürünü oluşturuyorsanız, yetkilendirme katmanı doğru yapılmaya değer ilk çapraz kesim sorunlarından biridir. Her platforma, her özellik kapısına ve her kullanıcı oturumuna dokunur. Burada temiz bir olay tabanlı mimariye yatırım yapmak, ürününüz büyüdükçe bileşik getiriler sağlar.

Bu mimariye beslenen ö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