Bu rehber, PocketBase backend’i üzerinde çalışan, Google Firebase Cloud Messaging (FCM) V1 API kullanarak Flutter uygulamasına hem manuel hem de zamanlanmış (otomatik) bildirimler gönderen modern bir sistemin nasıl kurulacağını anlatır.
🌟 Özellikler
-
Sunucusuz (Serverless): Ekstra bir Node.js veya Python sunucusu gerektirmez. Tüm mantık PocketBase Hooks (JS) içinde çalışır.
-
FCM V1 API: Google’ın en yeni ve güvenli protokolü (OAuth 2.0) kullanılır. Legacy API kapandığı için bu yöntem zorunludur.
-
Otomasyon: Her gün belirli saatte otomatik “Günün Sözü” bildirimi atar.
-
Kontrol Paneli: Otomatik mesajlar veritabanından tek tuşla açılıp kapatılabilir.
-
Güvenlik: API uç noktaları gizli bir
Secret Keyile korunur.
BÖLÜM 1: Google Cloud Kurulumu (Kritik Adım)
Google artık statik “Server Key” vermiyor. Dinamik token üretmek için OAuth kurulumu şart.
-
Google Cloud Console‘a gidin ve Firebase projenizi seçin.
-
APIs & Services > Credentials menüsünden + CREATE CREDENTIALS -> OAuth client ID seçin.
-
Type: Web application.
-
Redirect URI:
https://developers.google.com/oauthplayground(Bu adres zorunludur).
-
-
Oluşan Client ID ve Client Secret‘ı not edin.
-
OAuth Consent Screen menüsüne gidin ve uygulamanızı “PUBLISH APP” butonuna basarak yayına alın. (Bunu yapmazsanız Token 7 gün sonra ölür).
-
Google OAuth Playground‘a gidin:
-
Ayarlardan kendi Client ID/Secret’ınızı girin.
-
Scope olarak
https://www.googleapis.com/auth/firebase.messagingseçin. -
Exchange authorization code for tokens diyerek
Refresh Tokenalın.
-
BÖLÜM 2: PocketBase Veritabanı Şeması
Admin panelinden şu koleksiyonları (tabloları) oluşturun:
1. duyurular (Log Kayıtları)
Gönderilen bildirimlerin yedeğini tutar.
-
baslik (Text)
-
icerik (Text)
2. sozler (İçerik Havuzu)
Otomatik botun rastgele seçeceği cümleler.
-
metin (Text)
-
(İçine 5-10 tane güzel söz ekleyin)
3. sistem_ayarlari (Kontrol Merkezi)
Sistemi kod değiştirmeden yönetmek için.
-
otomatik_bildirim_aktif (Bool) -> Sistemi aç/kapa.
-
son_gonderim_tarihi (Date) -> Günde 1 kez kontrolü için.
-
(Sadece 1 adet kayıt oluşturun ve ID’sini not edin).
BÖLÜM 3: Backend Kodları (Hooks)
Bu kodlar PocketBase sunucusunda (örneğin Coolify terminalinde) çalıştırılacaktır. Dosya yollarına (/app/pb_hooks/) dikkat edilmelidir.
3.1. Manuel Bildirim Servisi (broadcast.pb.js)
Bu servis, dışarıdan (Postman, Flutter Admin App) gelen istekleri karşılar ve anlık bildirim atar.
// Dosya Yolu: /app/pb_hooks/broadcast.pb.js
routerAdd("POST", "/api/duyuru-yap", (c) => {
// --- KONFIGURASYON ---
const CLIENT_ID = "SİZİN_CLIENT_ID";
const CLIENT_SECRET = "SİZİN_CLIENT_SECRET";
const REFRESH_TOKEN = "SİZİN_REFRESH_TOKEN";
const PROJECT_ID = "FIREBASE_PROJE_ID"; // örn: justtalk-1234
const ADMIN_API_KEY = "GIZLI_ADMIN_SIFRENIZ"; // örn: Just+Talk#2025
try {
// 1. GÜVENLİ VERİ OKUMA
let title = "Başlık Yok";
let body = "İçerik Yok";
let secret = "";
try {
const rawBody = readerToString(c.request.body);
if (rawBody) {
const json = JSON.parse(rawBody);
if (json.title) title = json.title;
if (json.body) body = json.body;
if (json.secret) secret = json.secret;
}
} catch (e) {}
// 2. GÜVENLİK DUVARI (BODYGUARD)
if (secret !== ADMIN_API_KEY) {
return c.json(403, { error: "Yasaklı Giriş! Şifre Yanlış." });
}
// 3. GOOGLE TOKEN ALMA
const tokenResponse = $http.send({
url: "https://oauth2.googleapis.com/token",
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token`
});
if (tokenResponse.statusCode !== 200) return c.json(500, { error: "Token Hatası" });
const accessToken = tokenResponse.json.access_token;
// 4. BİLDİRİM GÖNDERME (Herkese)
const fcmResponse = $http.send({
url: `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`,
method: "POST",
headers: {
"Authorization": "Bearer " + accessToken,
"Content-Type": "application/json"
},
body: JSON.stringify({
"message": {
"topic": "tum_kullanicilar",
"notification": { "title": title, "body": body }
}
})
});
// 5. LOGLAMA
if (fcmResponse.statusCode === 200) {
try {
const collection = $app.findCollectionByNameOrId("duyurular");
const record = new Record(collection);
record.set("baslik", title);
record.set("icerik", body);
$app.save(record);
} catch (e) {}
}
return c.json(fcmResponse.statusCode, fcmResponse.json);
} catch (err) {
return c.json(500, { error: err.toString() });
}
});
3.2. Otomatik Zamanlayıcı (scheduler.pb.js)
Bu servis her dakika uyanır, veritabanını kontrol eder ve şartlar uygunsa rastgele bir söz seçip gönderir.
// Dosya Yolu: /app/pb_hooks/scheduler.pb.js
// Her dakika çalış (* * * * *)
cronAdd("otomatik_olumlama", "* * * * *", (c) => {
// --- KONFIGURASYON ---
const CLIENT_ID = "SİZİN_CLIENT_ID";
const CLIENT_SECRET = "SİZİN_CLIENT_SECRET";
const REFRESH_TOKEN = "SİZİN_REFRESH_TOKEN";
const PROJECT_ID = "FIREBASE_PROJE_ID";
const AYAR_ID = "SISTEM_AYARLARI_KAYIT_ID"; // PocketBase'deki ID
try {
// 1. KONTROL: Bugün gönderildi mi? Sistem açık mı?
let ayarKaydi;
try { ayarKaydi = $app.findRecordById("sistem_ayarlari", AYAR_ID); } catch(e) { return; }
if (ayarKaydi.getBool("otomatik_bildirim_aktif") !== true) return;
const sonTarihStr = ayarKaydi.getString("son_gonderim_tarihi");
if (sonTarihStr) {
const sonTarih = new Date(sonTarihStr);
const bugun = new Date();
if (sonTarih.getDate() === bugun.getDate() &&
sonTarih.getMonth() === bugun.getMonth() &&
sonTarih.getFullYear() === bugun.getFullYear()) {
return; // Bugün zaten gönderilmiş.
}
}
// 2. İÇERİK SEÇİMİ (Rastgele)
let secilenSoz = "Harika bir gün!";
const records = $app.findAllRecords("sozler");
if (records.length > 0) {
const randomIndex = Math.floor(Math.random() * records.length);
secilenSoz = records[randomIndex].getString("metin");
}
// 3. TOKEN ALMA
const tokenResponse = $http.send({
url: "https://oauth2.googleapis.com/token",
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token`
});
if (tokenResponse.statusCode !== 200) return;
const accessToken = tokenResponse.json.access_token;
// 4. GÖNDERME (Kanal: gunluk_olumlama)
const fcmResponse = $http.send({
url: `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`,
method: "POST",
headers: {
"Authorization": "Bearer " + accessToken,
"Content-Type": "application/json"
},
body: JSON.stringify({
"message": {
"topic": "gunluk_olumlama",
"notification": { "title": "Günün Sözü ✨", "body": secilenSoz }
}
})
});
// 5. MÜHÜRLEME (Tarihi Güncelle)
if (fcmResponse.statusCode === 200) {
ayarKaydi.set("son_gonderim_tarihi", new Date().toISOString());
$app.save(ayarKaydi);
console.log("✅ Günlük bildirim başarıyla gönderildi.");
}
} catch (err) {
console.log("🔥 Cron Hatası: " + err.toString());
}
});
BÖLÜM 4: Flutter Entegrasyonu
4.1. Ayarlar Sayfası (Topic Aboneliği)
Kullanıcının günlük bildirimleri açıp kapatabilmesi için.
// Switch değiştiğinde çalışacak fonksiyon
Future<void> _bildirimAyariniDegistir(bool acikMi) async {
final fcm = FirebaseMessaging.instance;
if (acikMi) {
await fcm.subscribeToTopic('gunluk_olumlama');
print("Abone olundu ✅");
} else {
await fcm.unsubscribeFromTopic('gunluk_olumlama');
print("Abonelikten çıkıldı ❌");
}
}
4.2. Admin Paneli (Manuel Gönderim)
Backend’deki şifre korumalı servise istek atma.
Future<void> _bildirimGonder() async {
final url = Uri.parse('https://SİZİN-DOMAIN.com/api/duyuru-yap');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'title': 'Test Başlık',
'body': 'Test İçerik',
'secret': 'Just+Talk#2025' // Backend'deki şifre ile aynı olmalı
}),
);
if (response.statusCode == 200) {
print("Gönderildi! 🚀");
} else {
print("Hata: ${response.body}");
}
}
💡 İpuçları ve Sorun Giderme
-
Klasör Yolu: Coolify veya Docker ortamında dosyalar her zaman
/app/pb_hooks/klasöründe olmalıdır. Terminaldemkdir -p /app/pb_hookskomutunu kullanarak klasörü garantiye alın. -
Restart:
.jsdosyalarında yapılan her değişiklikten sonra PocketBase sunucusu Restart edilmelidir. -
Cron Formatı: PocketBase Goja motoru 5 yıldızlı formatı (
* * * * *) destekler.@everygibi makrolar bazı sürümlerde çalışmayabilir. -
Veritabanı Güvenliği: JS Hook’ları
$appobjesini kullandığı için Admin yetkisine sahiptir, API Rules kısıtlamalarına takılmaz.