İçeriğe atla

2026-04-02

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

Bu yazı, AWS EventBridge üzerinde bu katmanın nasıl tasarlanacağını gösterir. Stripe, App Store Server Notifications v2 ve Google Play RTDN arasında olay normalizasyonunu; idempotent webhook handler’ları; DynamoDB yetkilendirme deposunu; ve platformlar arası abonelikleri üretimde kıran uç durumları (iadeler, aile paylaşımı, tolerans süreleri) ele alır.

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?”

Server Notification v2

RTDN Pub/Sub

Webhook Events

Apple App Store

Webhook Giris

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

// Yetkilendirmeler icin DynamoDB tablo tasarimi
interface 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.

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 tarafinda akilli onbellekli yetkilendirme kontrolu
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;
  }

  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.

// Webhook giris -- hizli onay, asenkron isleme
import { 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.

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.

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.

Basari

Yeniden denemeler sonrasi basarisizlik

Alarm

Tekrar Oynat

EventBridge Kurali

Yetkilendirme Lambda

DynamoDB Guncelleme

SQS DLQ

DLQ Islemci Lambda

CloudWatch Alarmi

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.

// Yetkilendirme pipeline'i icin CDK altyapisi
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);

    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.

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.

// Zamanlanmis uzlastirma -- her 6 saatte bir calisir
async 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

Abonelik Yaşam Döngüsü Yönetimi: Yükseltmeler, Dunning ve Dolandırıcılık Tespiti

Abonelik durum makineleri, proration stratejileri, dunning yönetimi ve dolandırıcılık tespit kalıpları hakkında Stripe webhook'ları ve AWS EventBridge ile pratik bir rehber.

subscriptionsstripefraud-detection+4
wasmCloud + NATS: Kilitlenme Aslında Event Bus Topolojisinde Yaşar

Bir keşif tezi: event-driven sistemlerde vendor lock-in runtime katmanında değil, bus topolojisinde yaşar; wasmCloud ve NATS ise bus'ı taşınabilir bir primitif haline getiriyor.

wasmcloudnatsevent-driven+4
AWS Mesajlaşma Servisleri: SQS vs SNS vs EventBridge - Karar Verme Çerçevesi

Özelliklere göre değil, iletişim modeline göre seçim yap. SQS, SNS ve EventBridge arasında karar vermek için pratik bir rehber; çalışan CDK örnekleri ve maliyet analizi ile.

aws-sqsaws-snsaws-eventbridge+5
TypeScript Geliştiricilerin Monolitten Lambda'ya Taşıdığı Beş Anti-Pattern

DI container'lar, monolitik SDK'lar, god-handler'lar, modül üstü secret çağrıları ve ağır ORM'ler - soğuk başlatmada bedeli ve yerine geçen fonksiyonel yapı.

aws-lambdatypescriptserverless+2
Web ve Mobil için Asenkron API Desenleri: Görüşlü Bir Varsayılan

Tek bir backend üzerinde çalışan web SPA ve mobil uygulama için uzun süreli işlere dair tek bir varsayılan desen ve onu geçersiz kılmanız gereken durumlar.

api-designwebsocketsserver-sent-events+5