Reusable FCM / Background Job Notification System Blueprint

This document explains a reusable notification architecture that can be implemented in any backend project. The reference stack is Firebase Cloud Messaging (FCM), a background job scheduler such as Hangfire, a relational database, and a backend service layer. The same design can be adapted to .NET, Node.js, Java, Go, Python, or any other stack with equivalent components.

Language options:

Universal Architecture

[Diagram]

Core Building Blocks

Component Purpose Can Be Implemented With
Device token API Receives and removes client FCM tokens REST, GraphQL, gRPC
Device token store Keeps token lifecycle and limits SQL table, document DB, key-value store
Notification producer Decides who should receive which message Service class, domain handler, scheduled worker
Background scheduler Runs periodic checks and delayed sends Hangfire, Quartz, BullMQ, Celery, Sidekiq, Cloud Tasks
Timezone filter Sends only during allowed local hours GMT offset, IANA timezone id, NodaTime/date-time library
FCM sender Sends push notifications to Firebase Firebase Admin SDK
Retry policy Retries transient failures safely Scheduler retry, app retry, resilience library
Progress checkpoint Prevents duplicate processing SQL checkpoint row, event offset, outbox table
Feature toggle Turns notification types on/off DB setting, config file, feature flag service
Observability Helps inspect jobs, failures, and token health Logs, metrics, dashboard, SQL queries

Türkçe

Amaç

Bu dokümanın amacı, FCM tabanlı notification sistemini belirli bir projeye bağlı kalmadan anlatmaktır. Dokümanı okuyan bir geliştirici, kendi projesinde aynı mimariyi kurabilmelidir.

Sistem şu ihtiyaçları karşılar:

  1. Kullanıcı cihazlarından gelen FCM token'larını güvenli şekilde saklamak.
  2. Yeni notification gerektiren durumları event veya periyodik kontrol ile yakalamak.
  3. Kullanıcının yerel saatine göre mesajı hemen göndermek veya uygun zamana ertelemek.
  4. Başarılı gönderimleri, başarısız denemeleri, günlük limitleri ve geçersiz token'ları yönetmek.
  5. Sistemi feature toggle, log ve job geçmişi ile operasyonel olarak yönetilebilir hale getirmek.

Ne Zaman Bu Mimari Kullanılmalı?

Bu mimari özellikle şu durumlarda faydalıdır:

Önerilen Bileşenler

Her projede aşağıdaki bileşenler kurulmalıdır:

Bileşen Sorumluluk
DeviceTokenController veya API endpoint Client'tan token register/unregister isteğini alır
DeviceTokenService Token doğrulama, ownership kontrolü, aktif/pasif yönetimi yapar
DeviceTokenRepository Token kayıtlarını veritabanında yönetir
NotificationService Notification adaylarını bulur, timezone filtresi uygular, gönderimi başlatır
NotificationSender Firebase Admin SDK ile FCM gönderimi yapar
NotificationScheduler Uygun olmayan saatlerdeki notification'ları ileri tarihe alır
NotificationProgressStore Periyodik işlerde son başarılı işleme noktasını tutar
FeatureToggleService Notification tiplerinin aktif/pasif durumunu okur
BackgroundJobWorker Periyodik ve scheduled job'ları çalıştırır

Bu isimler zorunlu değildir. Önemli olan sorumlulukların ayrılmasıdır.

Veritabanı Modeli

Minimum model iki tabloyla kurulabilir: DeviceTokens ve NotificationProgress.

DeviceTokens

Bu tablo cihaz token yaşam döngüsünü tutar.

CREATE TABLE DeviceTokens
(
    Id BIGINT IDENTITY(1,1) PRIMARY KEY,
    UserId BIGINT NOT NULL,
    Token NVARCHAR(MAX) NOT NULL,
    TokenHash NVARCHAR(128) NULL,
    Platform NVARCHAR(50) NOT NULL,
    NotificationCount INT NOT NULL DEFAULT 0,
    LastSentAtUtc DATETIME2 NULL,
    CreatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    IsActive BIT NOT NULL DEFAULT 1
);

CREATE INDEX IX_DeviceTokens_User_Active
ON DeviceTokens(UserId, IsActive);

CREATE INDEX IX_DeviceTokens_User_Platform_Active
ON DeviceTokens(UserId, Platform, IsActive);

Öneriler:

NotificationProgress

Bu tablo periyodik notification işlerinde checkpoint tutar.

CREATE TABLE NotificationProgress
(
    NotificationType NVARCHAR(100) NOT NULL PRIMARY KEY,
    LastProcessedValue NVARCHAR(MAX) NULL,
    SuccessRunTimeUtc DATETIME2 NOT NULL,
    LastRunStatus BIT NOT NULL,
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);

Bu tablo özellikle şu senaryoda gerekir: "Her 15 dakikada bir yeni kayıt var mı diye bak ve varsa notification gönder." Son başarılı çalışma zamanı bilinmezse aynı veri tekrar tekrar işlenebilir.

Token Register / Unregister Akışı

Register endpoint örneği:

POST /api/device-tokens/register
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "userId": 123,
  "token": "fcm_device_token",
  "platform": "ios"
}

Register sırasında yapılması gerekenler:

  1. Request authenticated olmalı.
  2. Request içindeki userId, authenticated user ile eşleşmeli.
  3. token boş olmamalı.
  4. platform kabul edilen değerlerden biri olmalı: ios, android, web gibi.
  5. Aynı kullanıcı/platform için eski aktif token varsa pasiflenmeli.
  6. Yeni token aktif olarak kaydedilmeli.
  7. Eski token'ın NotificationCount ve LastSentAtUtc gibi bilgileri gerekiyorsa yeni token'a taşınmalı.

Unregister endpoint örneği:

POST /api/device-tokens/unregister
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "userId": 123,
  "token": "fcm_device_token",
  "platform": "ios"
}

Unregister sırasında token silinmemeli, pasife çekilmelidir:

UPDATE DeviceTokens
SET IsActive = 0,
    UpdatedAtUtc = SYSUTCDATETIME()
WHERE UserId = @UserId
  AND TokenHash = @TokenHash
  AND IsActive = 1;

Notification Payload Kontratı

Timezone-aware notification gönderebilmek için her notification payload'ı ortak bir kontrata uymalıdır.

public interface ITimezoneAwareNotification
{
    long UserId { get; }
    string Token { get; }
    string TimezoneId { get; }
    int? GmtOffsetSeconds { get; }
    string GetTitle();
    string GetBody();
    Dictionary<string, string> GetData();
}

Tercih sırası:

  1. Mümkünse TimezoneId saklayın: Europe/Istanbul, America/New_York.
  2. Sadece basit ihtiyaç varsa GmtOffsetSeconds kullanılabilir.
  3. Daylight saving time önemliyse sabit GMT offset yeterli değildir.

Örnek payload:

public sealed class NewCustomerNotification : ITimezoneAwareNotification
{
    public long UserId { get; init; }
    public string Token { get; init; }
    public string TimezoneId { get; init; }
    public int? GmtOffsetSeconds { get; init; }
    public string CustomerName { get; init; }

    public string GetTitle() => "New Customer";
    public string GetBody() => $"A new customer registered: {CustomerName}";
    public Dictionary<string, string> GetData() => new()
    {
        ["type"] = "new_customer"
    };
}

Timezone ve Sessiz Saat Kuralı

Temel kural:

Örnek algoritma:

public NotificationTiming DecideTiming(
    DateTime utcNow,
    TimeZoneInfo userTimezone,
    TimeOnly start,
    TimeOnly end)
{
    var localNow = TimeZoneInfo.ConvertTimeFromUtc(utcNow, userTimezone);
    var localTime = TimeOnly.FromDateTime(localNow);

    if (localTime >= start && localTime < end)
    {
        return NotificationTiming.SendNow();
    }

    var targetLocalDate = localTime < start
        ? localNow.Date
        : localNow.Date.AddDays(1);

    var targetLocal = targetLocalDate.Add(start.ToTimeSpan());
    var targetUtc = TimeZoneInfo.ConvertTimeToUtc(targetLocal, userTimezone);

    return NotificationTiming.ScheduleAt(targetUtc.AddMinutes(1));
}

Bu kural config'e alınmalıdır:

{
  "Notifications": {
    "AllowedLocalStartHour": 9,
    "AllowedLocalEndHour": 22,
    "MaxConcurrency": 20,
    "MaxRetries": 3
  }
}

Gönderim Pipeline'ı

Her notification tipi aşağıdaki pipeline'dan geçmelidir:

  1. Feature açık mı kontrol et.
  2. Notification adaylarını bul.
  3. Aktif ve limite takılmamış token'ları seç.
  4. Kullanıcının timezone bilgisini ekle.
  5. Adayları send now ve schedule later olarak ayır.
  6. Scheduled olanları background job kuyruğuna yaz.
  7. Immediate olanları concurrency limit ile gönder.
  8. Başarılı token'lar için LastSentAtUtc ve NotificationCount güncelle.
  9. Invalid token'ları pasifle.
  10. Progress/checkpoint bilgisini güncelle.

Concurrency Limit

Toplu notification gönderimlerinde aynı anda binlerce FCM isteği açmak risklidir. Uygulama ve Firebase tarafında basıncı kontrol etmek için concurrency limit kullanılmalıdır.

var semaphore = new SemaphoreSlim(maxConcurrency);

async Task<SendResult> SendOneAsync(NotificationPayload payload)
{
    await semaphore.WaitAsync();
    try
    {
        return await sender.SendAsync(payload);
    }
    finally
    {
        semaphore.Release();
    }
}

Önerilen başlangıç değeri 20 olabilir. Bu değer sistem yüküne göre ölçülüp değiştirilmelidir.

Retry ve FCM Hata Yönetimi

FCM gönderim sonucu string ile değil, typed result ile temsil edilmelidir.

public enum SendStatus
{
    Success,
    RetryableFailure,
    InvalidToken,
    PermanentFailure
}

public sealed record SendResult(
    SendStatus Status,
    string Message,
    string Token);

Retryable kabul edilebilecek FCM hataları:

Invalid token kabul edilecek durumlar:

Davranış:

Periyodik ve Event-Driven Notification Tipleri

İki farklı üretim modeli desteklenmelidir.

Periyodik model:

Event-driven model:

Sağlam sistemlerde event-driven notification için outbox pattern önerilir:

  1. Business işlem ve notification event aynı transaction içinde outbox'a yazılır.
  2. Background worker outbox'ı okur.
  3. Notification gönderilir.
  4. Outbox kaydı processed yapılır.

Scheduled Job Payload Tasarımı

Scheduled job'a doğrudan class adı veya runtime-specific type bilgisi yazmak uzun vadede risklidir. Class adı veya namespace değişirse eski job'lar bozulabilir.

Daha güvenli yaklaşım version'lı envelope kullanmaktır:

{
  "type": "new_customer",
  "version": 1,
  "payload": {
    "userId": 123,
    "token": "fcm_token",
    "timezoneId": "Europe/Istanbul",
    "customerName": "Ali Veli"
  }
}

Job çalışırken dispatcher type ve version alanına göre doğru handler'ı çağırır.

Operasyonel SQL Örnekleri

Aktif token'ları görmek:

SELECT UserId, Platform, IsActive, NotificationCount, LastSentAtUtc, UpdatedAtUtc
FROM DeviceTokens
WHERE UserId = @UserId
ORDER BY UpdatedAtUtc DESC;

Limite takılan token'ları görmek:

SELECT UserId, Platform, NotificationCount, LastSentAtUtc
FROM DeviceTokens
WHERE IsActive = 1
  AND NotificationCount >= @DailyLimit;

Progress görmek:

SELECT NotificationType, SuccessRunTimeUtc, LastRunStatus, UpdatedAtUtc
FROM NotificationProgress
ORDER BY NotificationType;

Kurulum Checklist'i

  1. Firebase projesi oluştur.
  2. Firebase Admin SDK credential'ını secret olarak backend'e ver.
  3. Background job altyapısını kur: Hangfire, Quartz, BullMQ, Celery veya benzeri.
  4. DeviceTokens tablosunu oluştur.
  5. NotificationProgress veya outbox tablosunu oluştur.
  6. Token register/unregister endpointlerini auth ile koru.
  7. Authenticated user ile request userId eşleşmesini zorunlu tut.
  8. Notification payload kontratını tanımla.
  9. Timezone ve sessiz saat kuralını config'e al.
  10. FCM sender servisini typed result dönecek şekilde yaz.
  11. Retryable, invalid token ve permanent failure ayrımını yap.
  12. Concurrency limit ekle.
  13. Scheduled job payload'ını version'lı envelope olarak tasarla.
  14. Feature toggle ekle.
  15. Log, metric ve job inspection imkanlarını ekle.
  16. İlk notification tipini uçtan uca test et: register, candidate, send, schedule, retry, invalid token.

Özet

Bu mimarinin özü şudur: Notification göndermek sadece FCM'e HTTP isteği atmak değildir. Güvenilir bir notification sistemi token lifecycle, timezone, scheduling, retry, rate limit, checkpoint ve operasyonel gözlemlenebilirlik parçalarının birlikte çalışmasıyla oluşur.

English

Goal

This document describes a reusable FCM notification architecture that can be implemented in any backend project. A developer should be able to read this document and build the same system in their own project, regardless of framework or language.

The system provides:

  1. Safe storage of FCM device tokens.
  2. Notification creation from domain events or periodic scans.
  3. Immediate send or delayed scheduling based on the user's local time.
  4. Tracking for successful sends, failures, daily limits, and invalid tokens.
  5. Operational control through feature toggles, logs, and job history.

Recommended Components

Component Responsibility
Device token API Receives token register/unregister requests
Device token service Validates ownership and manages active/inactive state
Device token repository Persists token records
Notification service Finds candidates, applies timezone rules, starts sending
Notification sender Sends messages through Firebase Admin SDK
Notification scheduler Schedules messages for a later allowed local time
Progress/checkpoint store Stores the last successful point for periodic jobs
Feature toggle service Enables or disables notification types
Background job worker Runs recurring and scheduled jobs

Names can vary by stack. The important part is the separation of responsibilities.

Data Model

A minimal implementation needs DeviceTokens and either NotificationProgress or an outbox table.

CREATE TABLE DeviceTokens
(
    Id BIGINT IDENTITY(1,1) PRIMARY KEY,
    UserId BIGINT NOT NULL,
    Token NVARCHAR(MAX) NOT NULL,
    TokenHash NVARCHAR(128) NULL,
    Platform NVARCHAR(50) NOT NULL,
    NotificationCount INT NOT NULL DEFAULT 0,
    LastSentAtUtc DATETIME2 NULL,
    CreatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    IsActive BIT NOT NULL DEFAULT 1
);
CREATE TABLE NotificationProgress
(
    NotificationType NVARCHAR(100) NOT NULL PRIMARY KEY,
    LastProcessedValue NVARCHAR(MAX) NULL,
    SuccessRunTimeUtc DATETIME2 NOT NULL,
    LastRunStatus BIT NOT NULL,
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);

Do not delete tokens by default. Mark them inactive. This preserves counters, last sent time, and historical behavior.

Device Token Flow

Register request:

POST /api/device-tokens/register
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "userId": 123,
  "token": "fcm_device_token",
  "platform": "ios"
}

Register rules:

  1. The request must be authenticated.
  2. The request userId must match the authenticated user.
  3. The token must not be empty.
  4. The platform must be valid.
  5. Previous active tokens for the same user/platform can be deactivated.
  6. The new token is saved as active.
  7. Existing counters may be carried over to the new token if your business rules require it.

Unregister should deactivate the token instead of deleting it.

Notification Payload Contract

Every timezone-aware notification should expose the data required for sending and scheduling.

public interface ITimezoneAwareNotification
{
    long UserId { get; }
    string Token { get; }
    string TimezoneId { get; }
    int? GmtOffsetSeconds { get; }
    string GetTitle();
    string GetBody();
    Dictionary<string, string> GetData();
}

Prefer TimezoneId such as Europe/Istanbul or America/New_York. Fixed GMT offsets are simpler but do not handle daylight saving time correctly.

Timezone and Quiet Hours

Default behavior:

These values should be configurable:

{
  "Notifications": {
    "AllowedLocalStartHour": 9,
    "AllowedLocalEndHour": 22,
    "MaxConcurrency": 20,
    "MaxRetries": 3
  }
}

Sending Pipeline

Each notification type should follow the same pipeline:

  1. Check whether the feature is enabled.
  2. Find notification candidates.
  3. Select active tokens that are under the daily limit.
  4. Attach timezone data.
  5. Split candidates into send now and schedule later.
  6. Persist scheduled items to the background job queue.
  7. Send immediate items with a concurrency limit.
  8. Update LastSentAtUtc and NotificationCount for successful sends.
  9. Deactivate invalid tokens.
  10. Update progress/checkpoint data.

Retry and FCM Error Handling

Use a typed result instead of returning raw strings.

public enum SendStatus
{
    Success,
    RetryableFailure,
    InvalidToken,
    PermanentFailure
}

public sealed record SendResult(
    SendStatus Status,
    string Message,
    string Token);

Usually retryable:

Usually invalid-token cases:

Behavior:

Periodic vs Event-Driven Notifications

Periodic notifications:

Event-driven notifications:

Outbox flow:

  1. The business transaction writes both domain data and a notification event.
  2. A worker reads the outbox.
  3. The notification is sent or scheduled.
  4. The outbox item is marked processed.

Scheduled Job Payload

Avoid storing runtime-specific class names as the long-term job contract. Use a versioned envelope.

{
  "type": "new_customer",
  "version": 1,
  "payload": {
    "userId": 123,
    "token": "fcm_token",
    "timezoneId": "Europe/Istanbul",
    "customerName": "Ali Veli"
  }
}

The job dispatcher can route by type and version.

Implementation Checklist

  1. Create a Firebase project.
  2. Provide Firebase Admin SDK credentials to the backend as a secret.
  3. Install a background job system such as Hangfire, Quartz, BullMQ, Celery, or equivalent.
  4. Create the DeviceTokens table.
  5. Create NotificationProgress or an outbox table.
  6. Protect token register/unregister endpoints with authentication.
  7. Enforce authenticated user and request userId matching.
  8. Define a notification payload contract.
  9. Make timezone and quiet-hour rules configurable.
  10. Implement the FCM sender with typed results.
  11. Separate retryable failures, invalid tokens, and permanent failures.
  12. Add a concurrency limit.
  13. Store scheduled job payloads as versioned envelopes.
  14. Add feature toggles.
  15. Add logs, metrics, and job inspection.
  16. Test one notification type end to end: register, candidate, send, schedule, retry, invalid token.

Summary

A reliable notification system is not just an FCM call. It is a coordinated pipeline for token lifecycle, timezone rules, scheduling, retry, rate limits, checkpointing, and operational visibility.

فارسی

هدف

این سند یک معماری قابل استفاده مجدد برای notification با FCM را توضیح می‌دهد. هدف این است که توسعه‌دهنده بتواند این سند را بخواند و همین سیستم را در پروژه خودش، با هر زبان یا framework، پیاده‌سازی کند.

این سیستم این نیازها را پوشش می‌دهد:

  1. ذخیره امن FCM device tokenها.
  2. تولید notification از روی eventهای business یا بررسی دوره‌ای داده‌ها.
  3. ارسال فوری یا زمان‌بندی ارسال بر اساس ساعت محلی کاربر.
  4. مدیریت ارسال موفق، خطا، محدودیت روزانه و token نامعتبر.
  5. کنترل عملیاتی با feature toggle، log و تاریخچه jobها.

اجزای پیشنهادی

جزء مسئولیت
Device token API دریافت درخواست register/unregister token
Device token service اعتبارسنجی مالکیت token و مدیریت active/inactive
Device token repository ذخیره tokenها در دیتابیس
Notification service پیدا کردن کاندیدها و اعمال قانون timezone
Notification sender ارسال پیام با Firebase Admin SDK
Notification scheduler زمان‌بندی پیام برای ساعت مجاز
Progress/checkpoint store ذخیره آخرین نقطه موفق برای jobهای دوره‌ای
Feature toggle service فعال یا غیرفعال کردن نوع‌های notification
Background job worker اجرای jobهای دوره‌ای و scheduled

نام کلاس‌ها مهم نیست. مهم این است که مسئولیت‌ها از هم جدا باشند.

مدل دیتابیس

حداقل پیاده‌سازی به جدول DeviceTokens و یکی از NotificationProgress یا outbox نیاز دارد.

CREATE TABLE DeviceTokens
(
    Id BIGINT IDENTITY(1,1) PRIMARY KEY,
    UserId BIGINT NOT NULL,
    Token NVARCHAR(MAX) NOT NULL,
    TokenHash NVARCHAR(128) NULL,
    Platform NVARCHAR(50) NOT NULL,
    NotificationCount INT NOT NULL DEFAULT 0,
    LastSentAtUtc DATETIME2 NULL,
    CreatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
    IsActive BIT NOT NULL DEFAULT 1
);
CREATE TABLE NotificationProgress
(
    NotificationType NVARCHAR(100) NOT NULL PRIMARY KEY,
    LastProcessedValue NVARCHAR(MAX) NULL,
    SuccessRunTimeUtc DATETIME2 NOT NULL,
    LastRunStatus BIT NOT NULL,
    UpdatedAtUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);

به صورت پیش‌فرض token را حذف نکنید. بهتر است آن را inactive کنید. این کار باعث می‌شود شمارنده‌ها و تاریخچه ارسال از بین نرود.

جریان Register و Unregister

نمونه register:

POST /api/device-tokens/register
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "userId": 123,
  "token": "fcm_device_token",
  "platform": "ios"
}

قوانین register:

  1. request باید authenticated باشد.
  2. userId داخل request باید با user احراز هویت‌شده یکی باشد.
  3. token نباید خالی باشد.
  4. platform باید معتبر باشد.
  5. tokenهای active قبلی برای همان user/platform می‌توانند inactive شوند.
  6. token جدید active ذخیره می‌شود.
  7. اگر business نیاز دارد، counterهای token قبلی به token جدید منتقل می‌شود.

در unregister بهتر است token حذف نشود و فقط inactive شود.

قرارداد Payload

هر notification که به timezone وابسته است باید داده‌های لازم برای ارسال و schedule را داشته باشد.

public interface ITimezoneAwareNotification
{
    long UserId { get; }
    string Token { get; }
    string TimezoneId { get; }
    int? GmtOffsetSeconds { get; }
    string GetTitle();
    string GetBody();
    Dictionary<string, string> GetData();
}

بهتر است از TimezoneId مثل Europe/Istanbul یا America/New_York استفاده شود. GMT offset ثابت ساده‌تر است، اما daylight saving time را درست مدیریت نمی‌کند.

Timezone و ساعت سکوت

قانون پیش‌فرض:

این مقادیر بهتر است configurable باشند:

{
  "Notifications": {
    "AllowedLocalStartHour": 9,
    "AllowedLocalEndHour": 22,
    "MaxConcurrency": 20,
    "MaxRetries": 3
  }
}

Pipeline ارسال

هر نوع notification باید از همین pipeline عبور کند:

  1. بررسی کن feature فعال است یا نه.
  2. کاندیدهای notification را پیدا کن.
  3. tokenهای active و زیر limit روزانه را انتخاب کن.
  4. timezone user را اضافه کن.
  5. کاندیدها را به send now و schedule later تقسیم کن.
  6. آیتم‌های scheduled را در background job queue ذخیره کن.
  7. آیتم‌های immediate را با concurrency limit ارسال کن.
  8. برای ارسال موفق، LastSentAtUtc و NotificationCount را به‌روزرسانی کن.
  9. tokenهای invalid را inactive کن.
  10. progress/checkpoint را به‌روزرسانی کن.

Retry و مدیریت خطای FCM

بهتر است نتیجه ارسال به صورت typed result باشد.

public enum SendStatus
{
    Success,
    RetryableFailure,
    InvalidToken,
    PermanentFailure
}

public sealed record SendResult(
    SendStatus Status,
    string Message,
    string Token);

خطاهای معمولا قابل retry:

حالت‌های token نامعتبر:

رفتار مناسب:

Notification دوره‌ای و Event-Driven

Notification دوره‌ای:

Notification event-driven:

جریان outbox:

  1. business transaction هم داده اصلی و هم notification event را ذخیره می‌کند.
  2. worker رکوردهای outbox را می‌خواند.
  3. notification ارسال یا schedule می‌شود.
  4. رکورد outbox processed می‌شود.

Payload برای Scheduled Job

بهتر است class name یا type runtime را به عنوان قرارداد اصلی job ذخیره نکنید. از envelope دارای version استفاده کنید.

{
  "type": "new_customer",
  "version": 1,
  "payload": {
    "userId": 123,
    "token": "fcm_token",
    "timezoneId": "Europe/Istanbul",
    "customerName": "Ali Veli"
  }
}

dispatcher می‌تواند بر اساس type و version handler مناسب را پیدا کند.

چک‌لیست پیاده‌سازی

  1. یک Firebase project بسازید.
  2. credential مربوط به Firebase Admin SDK را به صورت secret به backend بدهید.
  3. یک background job system مثل Hangfire، Quartz، BullMQ، Celery یا مشابه نصب کنید.
  4. جدول DeviceTokens را بسازید.
  5. جدول NotificationProgress یا outbox را بسازید.
  6. endpointهای register/unregister را با authentication محافظت کنید.
  7. تطابق user احراز هویت‌شده و userId داخل request را اجباری کنید.
  8. قرارداد payload برای notification تعریف کنید.
  9. قانون timezone و ساعت سکوت را configurable کنید.
  10. FCM sender را با typed result پیاده‌سازی کنید.
  11. خطاهای retryable، invalid token و permanent failure را جدا کنید.
  12. concurrency limit اضافه کنید.
  13. payload مربوط به scheduled job را versioned envelope ذخیره کنید.
  14. feature toggle اضافه کنید.
  15. log، metric و امکان inspect کردن jobها را اضافه کنید.
  16. یک notification type را end-to-end تست کنید: register، candidate، send، schedule، retry، invalid token.

خلاصه

یک notification system قابل اعتماد فقط یک call به FCM نیست. این سیستم از token lifecycle، timezone، scheduling، retry، rate limit، checkpoint و observability تشکیل می‌شود.