Konum Tabanlı Push Notification: Sıfırdan Kendi Bildirim Altyapınızı Kurmak

Mobil uygulamanızda push notification sistemi kurmak istediğinizde ilk akla gelen genellikle OneSignal, Firebase Cloud Messaging, Braze veya Dengage gibi third-party servisler oluyor. Bu araçlar hızlı başlangıç için cazip. Ama "kullanıcının anlık konumuna göre bildirim gönder" dediğiniz anda işler değişiyor; hiçbiri bunu hazır sunmuyor.
Kariyer, sipariş takibi, teslimat gibi Tam Konum (Precise Location) datasına sahip olması gereken uygulamalarda bu ihtiyaç kaçınılmaz: kullanıcı belirli bir bölgedeyken o bölgedeki yeni iş ilanını, yakınındaki kampanyayı ya da teslimat durumunu anında bildirmek istiyorsunuz. Bu yazıda APNs ve FCM API'lerine doğrudan bağlanan, cihazdan anlık konum toplayan ve PostGIS ile geo-targeted bildirim gönderebilen bir sistemi sıfırdan nasıl kurduğumu anlatacağım. Hem React Native (mobil) hem NestJS (backend) tarafındaki mimariyi, kodlarıyla birlikte inceleyeceğiz.
Hangi Durumlarda Kendi Notification Sistemimizi Kurmalıyız?
- Konum tabanlı hedefleme: OneSignal ve Braze gibi servisler geofence tabanlı bildirim sunuyor: önceden bir alan çizersiniz, kullanıcı o alana girdiğinde tetiklenir. Ancak amacımız yeni bir içerik yayınlandığında (iş ilanı, kampanya, teslimat noktası) sunucu tarafında yakındaki kullanıcıları bulup otomatik push göndermekse, bu "içerik tetiklemeli, sunucu taraflı geo-matching" akışını hazır sunan bir servis yok.
- Maliyet kontrolü: Third-party servislerde cihaz başına veya bildirim başına ücretlendirme var. APNs ve FCM ile kendi bildirim altyapınızı kurmak ise tamamen ücretsiz.
- Veri sahipliği: Kullanıcı cihaz bilgileri, konum verileri ve bildirim tercihleri tamamen kendi veritabanımızda.
- Esneklik: Sessiz (silent) bildirimler, çoklu uygulama desteği gibi ihtiyaçları üçüncü taraf arayüzlerinden bağımsız çözebiliyoruz.
- Vendor lock-in yok: Altyapıyı istediğimiz zaman taşıyabilir veya değiştirebiliriz.
Mimari Genel Bakış
Bu yazıda sistemi React Native (mobil), GraphQL (API gateway) ve NestJS (bildirim mikroservisi) üzerinden anlatacağım; iletişim katmanı olarak da RabbitMQ kullanıyorum. Siz kendi teknoloji yığınınıza göre rahatlıkla uyarlayabilirsiniz.

Mobil Taraf: Cihaz Kaydı ve Token Yönetimi
Push notification sisteminin mobil tarafı üç temel sorumluluk üzerine kurulu: izin alma, platform token'ı edinme ve backend'e kayıt. Tüm bu işleri bir React Provider bileşeninde yönetiyoruz.
Stabil Cihaz Kimliği
Her cihazın backend'de benzersiz bir kaydı olması gerekiyor. Ama DeviceInfo.getUniqueId() bazı durumlarda uygulama silme/yükleme sonrası değişebiliyor. Bu yüzden iki katmanlı bir yaklaşım izliyoruz:
const KEYCHAIN_SERVICE = 'company.device.id';
export const getOrCreatePushDeviceId = async () => {
// 1. Keychain'den dene (uygulama silinse bile kalır)
const stored = await Keychain.getGenericPassword({
service: KEYCHAIN_SERVICE
});
if (stored?.password) return stored.password;
// 2. OS'ten al, Keychain'e kaydet
const osDeviceId = await DeviceInfo.getUniqueId();
if (osDeviceId) {
await Keychain.setGenericPassword(KEYCHAIN_ACCOUNT, osDeviceId, {
service: KEYCHAIN_SERVICE,
accessible: Keychain.ACCESSIBLE.ALWAYS,
});
return osDeviceId;
}
return null;
};iOS'ta Keychain uygulama silme sonrasında bile verileri korur (aynı provisioning profile ile yeniden yüklendiğinde). Android'de ise Keystore üzerinden benzer bir kalıcılık sağlanıyor. Bu sayede kullanıcı uygulamayı silip tekrar yüklese bile aynı cihaz ID'siyle tanınıyor.
Platform Token'ı Alma: APNs vs FCM
iOS ve Android'de push token alma süreci farklı çalışıyor. iOS'ta önce APNs'e kayıt olup APN token'ını almak gerekiyor. Bu token bazen hemen hazır olmadığı için retry mekanizması ekliyoruz:
export const getPushToken = async () => {
if (Platform.OS === 'ios') {
const status = await messaging().hasPermission();
if (status === AuthorizationStatus.NOT_DETERMINED) {
await messaging().requestPermission();
}
await messaging().registerDeviceForRemoteMessages();
const apnsToken = await getApnsTokenWithRetry();
return { pushProvider: 'APNS', pushToken: apnsToken };
}
// Android: FCM token'ı doğrudan alınır
const fcmToken = await messaging().getToken();
return { pushProvider: 'FCM', pushToken: fcmToken };
};APNs token'ı neden hemen gelmez? iOS'ta
registerDeviceForRemoteMessages()çağrısı asenkrondur ve token sistem tarafından hazırlandığında gelir. Soğuk başlangıçta (cold start) bu birkaç yüz milisaniye sürebilir. Bu yüzden[0, 400, 800, 1500, 2500]ms aralıklarla 5 deneme yapıyoruz.
PushProvider: Orkestrasyon Katmanı
Tüm bu parçaları bir araya getiren bileşen PushProvider. Uygulama açıldığında, token yenilendiğinde, uygulama ön plana geldiğinde veya oturum değiştiğinde cihaz kaydını tetikliyor:
export const PushProvider = ({ children }) => {
const { tokens, user } = useAppSelector(state => state.auth);
const location = useAppSelector(state => state.device.location);
// Uygulama açılışında kayıt
useEffect(() => {
requestNotificationPermission();
syncDevice(undefined, { reason: 'boot' });
}, []);
// Firebase token yenilendiğinde
useEffect(() => {
return messaging().onTokenRefresh(() => {
syncDevice(undefined, { reason: 'token-refresh' });
});
}, []);
// Oturum kapandığında userId'yi null yap
useEffect(() => {
if (!isAuthenticated) {
syncDevice(null, { reason: 'auth-reset' });
}
}, [isAuthenticated]);
// Uygulama ön plana geldiğinde (30sn cooldown ile)
useEffect(() => {
const sub = AppState.addEventListener('change', state => {
if (state !== 'active') return;
if (Date.now() - lastCheck < 30000) return;
syncDevice(undefined, { reason: 'app-active' });
});
return () => sub.remove();
}, []);
return <>{children}</>;
};Deduplikasyon: Gereksiz API Çağrılarını Engelleme
Her senkronizasyonda tüm cihaz bilgilerini (model, OS, izinler, konum, token) birleştirip bir "imza" oluşturuyoruz. Bu imza bir önceki kayıtla aynıysa API çağrısı yapılmıyor:
const signature = createPayloadSignature(payload);
const lastSignature = await storage.getItem(
STORAGE_KEYS.PUSH_LAST_SYNC_SIGNATURE
);
if (lastSignature === signature && registrationId) {
return payload; // Değişiklik yok, API çağrısı atlanır
}İmza oluşturulurken syncedAt alanı sıfırlanıyor. Böylece sadece gerçek veri değişiklikleri (yeni token, konum güncellenmesi, izin değişikliği) kayda neden oluyor.
Apollo Client Entegrasyonu
Backend'e her GraphQL isteğinde cihaz kimliğini gönderiyoruz. Apollo Client'a özel bir middleware ekleyerek, her isteğe cx-device-id header'ı ekleniyor. Kayıt ID'si yoksa otomatik olarak cihaz senkronizasyonu tetikleniyor.
Retry Stratejisi
Ağ hataları veya token hazır olmama durumları için exponential backoff + jitter stratejisi uyguluyoruz. Bekleme süreleri: 5sn → 15sn → 45sn → 2dk → 5dk. Her süreye 0.85x–1.15x arası rastgele bir çarpan uygulanıyor. Bu, birden fazla cihazın aynı anda retry yapmasını (thundering herd) engelliyor.
Native Konfigürasyon
iOS Tarafı:
- Entitlements dosyası:
aps-environmentanahtarıdevelopmentveyaproductionolarak ayarlanmalı. - AppDelegate'te Firebase başlatma:
FirebaseApp.configure()çağrısı yeterli. APNs delegate metotlarını@react-native-firebase/messagingmethod swizzling ile otomatik yönetiyor. - GoogleService-Info.plist: Firebase projesinden indirilen yapılandırma dosyası. GCM etkinleştirilmiş olmalı.
Not: Bildirim izni için
react-native-permissionskullanmıyoruz. iOS tarafında Firebase Messaging kendi izin yönetimini yapıyor. Android 13+ (API 33) için isePermissionsAndroid.request(POST_NOTIFICATIONS)yeterli.
Android Tarafı:
google-services.json dosyası ve birkaç manifest izni gerekiyor. WAKE_LOCK iznindeki tools:remove="android:maxSdkVersion" kritik: Firebase'in ReactNativeFirebaseMessagingReceiver bileşeninin arka planda bildirimleri işleyebilmesi için API 26+ üzerinde de bu iznin geçerli olması gerekiyor.
Backend: APNs ve FCM'e Doğrudan Bağlantı
Bildirim servisini ana uygulamadan bağımsız bir mikroservis olarak ayırdım. Ana uygulama ile iletişimi RabbitMQ üzerinden asenkron mesajlaşma ile sağlıyorum; böylece bildirim gönderimi ana iş akışını bloke etmiyor.
APNs Entegrasyonu: HTTP/2 ile Doğrudan Bağlantı
Apple'ın APNs servisine herhangi bir SDK veya wrapper kullanmadan, Node.js'in native http2 modülü ile bağlanıyoruz. Kimlik doğrulama için JWT (ES256) kullanıyoruz:
import * as http2 from 'http2';
import * as jwt from 'jsonwebtoken';
private getJWT(creds: ApnsCredentials): string {
const privateKey = fs.readFileSync(creds.keyPath, 'utf8');
return jwt.sign({}, privateKey, {
algorithm: 'ES256',
keyid: creds.keyId,
issuer: creds.teamId,
expiresIn: '1h',
});
}
async send(deviceToken, payload) {
const host = isProduction
? 'api.push.apple.com'
: 'api.sandbox.push.apple.com';
const client = http2.connect(`https://${host}`);
const req = client.request({
':method': 'POST',
':path': `/3/device/${deviceToken}`,
'authorization': `bearer ${jwt}`,
'apns-topic': creds.bundleId,
'apns-push-type': payload.silent ? 'background' : 'alert',
'apns-priority': payload.silent ? '5' : '10',
});
}JWT caching: APNs JWT token'ları 1 saat geçerli. Biz 50 dakika boyunca cache'liyoruz, böylece her bildirimde yeniden imzalama yapılmıyor. Cache anahtarı
keyId:teamIdçifti; birden fazla uygulama için farklı credential'ları destekliyor.
FCM Entegrasyonu: v1 HTTP API
Android bildirimleri için FCM v1 API kullanıyoruz. Legacy API'den (sunucu anahtarı ile çalışan eski yöntem) farklı olarak, v1 API OAuth2 kimlik doğrulaması gerektiriyor ve daha zengin bir payload yapısı sunuyor:
import { GoogleAuth } from 'google-auth-library';
async send(deviceToken, payload) {
const auth = new GoogleAuth({
keyFile: creds.serviceAccountPath,
scopes: ['https://www.googleapis.com/auth/firebase.messaging'],
});
const accessToken = await auth.getClient().getAccessToken();
await fetch(
`https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`,
{
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ message: { token: deviceToken, ... } }),
}
);
}Payload Yapısı: iOS vs Android
Her iki platformun beklediği payload formatı farklı. Backend aynı içerikten iki farklı yapı üretiyor:
iOS (APNs):
{
"aps": {
"alert": {
"title": "Bölgende yeni fırsat!",
"body": "Sana yakın yeni içerikler var."
},
"sound": "default"
}
}Android (FCM v1):
{
"message": {
"token": "fcm_token...",
"notification": {
"title": "Bölgende yeni fırsat!",
"body": "Sana yakın yeni içerikler var."
},
"android": {
"priority": "high",
"notification": { "sound": "default" }
}
}
}Çoklu Uygulama Desteği: Credential Resolver
Sistemimiz birden fazla uygulamaya bildirim gönderebiliyor. Bunu PushCredentialResolver servisi ile yönetiyoruz. Çözümleme öncelik sırası: platform:appId → appId → global environment değişkenleri. Config dosyası yoksa veya bozuksa sistem eski davranışa (global env) geri dönüyor. Mevcut bildirimler hiçbir zaman bozulmuyor.
Cihaz Kaydında Toplanan Veriler
Her senkronizasyonda cihazdan kapsamlı bilgiler toplanıp backend'e gönderiliyor:
| Kategori | Alanlar |
|---|---|
| Kimlik | deviceId, platform, pushProvider, pushToken, userId |
| Uygulama | appId, appVersion, buildNumber |
| Cihaz | phoneType, deviceModel, manufacturer, osName, osVersion, osApiLevel |
| Ağ | timezone, locale |
| Bildirim İzinleri | permissionStatus, appNotificationsEnabled + platform flags |
| Konum | latitude, longitude, accuracy, capturedAt |
Backend bu verileri Prisma ile PostgreSQL'de saklıyor. Konum verisi zaman serisi olarak son 10 snapshot tutularak saklanıyor.
Bildirim Pipeline'ı: Uçtan Uca Akış
Bir bildirim tetiklenmesinden kullanıcının telefonuna ulaşmasına kadar geçen süreç:
- Domain event tetiklenir: Örneğin yeni bir kampanya yayınlanır veya kullanıcı belirli bir konuma girer. İlgili consumer servisi bu eventi yakalar.
- Template çözümlenir: Bildirim template'i dil ve tür bazında veritabanından çekilir. Mustache ile dinamik alanlar doldurulur.
- In-App kaydı oluşturulur: Veritabanına bir
NotificationUserkaydı eklenir. - Job kuyruğa eklenir:
NotificationJoboluşturulur, öncelik hesaplanır (Urgent → 0,Low → 4) ve RabbitMQ'ya gönderilir. - Queue worker işler: Mesaj alınır, kanal türüne göre (Email / SMS / InApp) yönlendirilir.
- Push gönderilir: Kullanıcının tüm kayıtlı cihazları çekilir. iOS'a APNs, Android'e FCM üzerinden
Promise.allSettled()ile paralel gönderilir. - Sonuç kaydedilir:
Sent/PartiallySent/Failed/NoDevicedurumu iş kaydına yazılır.
Retry Mekanizması
Başarısız gönderimler için backend'de 60 saniyede bir çalışan bir tarayıcı var. Atomic claiming ile çift işlemeyi önlüyor. Max 3 deneme ve 5 dakika aralıkla retry yapılıyor. 24 saatten eski job'lar expire ediliyor.
Konum Tabanlı Bildirimler: Sistemin Farkı
Bu sistemin en güçlü yanı, ve bizi third-party servislerden ayıran asıl özellik, anlık konum verisi toplama ve buna dayalı bildirim gönderme altyapısı.
Mobil tarafta her cihaz kaydı sırasında, kullanıcının izni varsa konum bilgisi de gönderiliyor. Uygulama her ön plana geldiğinde (30sn cooldown ile) konum güncelleniyor. Yani kullanıcı şehir değiştirse bile sistem bunu yakalar.
Backend tarafında PostgreSQL'de PostGIS eklentisi aktif. Konum snapshot'ları zaman serisi olarak saklanıyor (son 10 snapshot). Bu sayede sadece "şu an nerede" değil, "son X saatte nerelerde bulundu" bilgisine de erişebiliyoruz.
Pratikte nasıl çalışıyor? Uygulama zaten Elasticsearch üzerinde geo_distance sorguları kullanarak mesafeye göre içerik listeliyor. Bildirim tarafında da aynı mantık: yeni bir içerik yayınlandığında, o içeriğin konumuna yakın kullanıcılar filtreleniyor ve otomatik push gönderiliyor. Kullanıcı neredeyse, bulunduğu bölgedeki içeriklerden anında haberdar oluyor.
Bu akışı OneSignal, Braze, Dengage veya Firebase'in yönetilen katmanıyla kurmak mümkün değil. Çünkü hiçbiri cihazdan anlık konum toplayıp bunu bildirim hedeflemesiyle birleştiren bir pipeline sunmuyor.
Gerekli Dosyalar ve Credential'lar
| Dosya | Nereden Alınır | Ne İşe Yarar |
|---|---|---|
.p8 (APNs key) | Apple Developer → Keys | APNs JWT imzalama |
GoogleService-Info.plist | Firebase Console → iOS app | iOS Firebase bağlantısı |
google-services.json | Firebase Console → Android app | Android Firebase bağlantısı |
fcm.json (Service Account) | Google Cloud Console → IAM | FCM v1 API OAuth2 erişimi |
.p8 vs .p12: Apple iki tip sertifika sunuyor.
.p12(certificate-based) eski yöntem ve her yıl yenilenmesi gerekiyor..p8(token-based / JWT) daha yeni, süresiz ve tüm uygulamalar için tek key ile çalışabiliyor. Biz.p8kullanıyoruz.
Sonuç
Kendi push notification sisteminizi kurmak ilk bakışta göz korkutucu görünebilir ama aslında APNs ve FCM'in HTTP API'leri oldukça basit ve iyi belgelenmiş. Kritik olan kısımlar (token yönetimi, retry stratejileri, deduplikasyon ve çoklu cihaz desteği) iş mantığı seviyesinde çözülüyor.
Ama asıl kazanç şu: konum tabanlı bildirim gibi, hiçbir third-party servisin hazır sunmadığı özellikleri istediğiniz gibi inşa edebiliyorsunuz. Kullanıcı verileriniz sizde, pipeline tamamen sizin kontrolünüzde ve aylık cihaz başına ücret ödemiyorsunuz. Dezavantajı ise bakım sorumluluğunun tamamen size ait olması. Ama zaten production-grade bir uygulamada bu seviyede bir anlayış kaçınılmaz.
Eğer siz de konum verisini bildirimlerle birleştiren benzer bir sistem kurmayı düşünüyorsanız, bu yazının size iyi bir başlangıç noktası olmasını umuyorum. Sorularınız ya da önerileriniz için benimle iletişime geçebilirsiniz.