PWA ve Web Push Bildirim Sistemi
Tarayıcı kapalıyken bile bildirim gönderin — kütüphane kullanmadan, saf PHP ile. VAPID/ES256 imzası, ECDH + HKDF anahtar türetme ve aes128gcm uçtan uca şifreleme; çevrimdışı çalışan PWA kabuğuyla birlikte.
Ekran Görüntüleri 5 görsel
Bu Örnek Ne Yapıyor?
- Hiçbir push kütüphanesi kullanılmadı — VAPID ve şifreleme saf PHP'de
- VAPID JWT üretimi ve ES256 imzası; DER imza ham 64 bayta çevrilir
- ECDH ortak sır + HKDF ile içerik anahtarı ve nonce türetme
- aes128gcm (RFC 8188) ile uçtan uca şifreleme — push servisi içeriği okuyamaz
- Service worker altı olayı dinler; sekme kapalıyken de bildirim gelir
- Bildirime tıklayınca yeni sekme açılmaz, açık sekme öne getirilir
- Çevrimdışı gezinme yanıtı ve ana ekrana eklenebilir manifest
- 404/410 kesin ölüm sayılır ve satır silinir; geçici hatalarda sayaç artar
- Özel anahtar web'e kapalı storage/ klasöründe tutulur
Nerede İşe Yarar?
- Sitesine bildirim eklemek isteyip FCM SDK'sından kaçınanlar
- VAPID'in ne olduğunu ve neden gerektiğini öğrenmek isteyenler
- Web push şifrelemesinin adım adım nasıl yapıldığını görmek isteyenler
- Uygulamasını PWA'ya çevirip ana ekrana eklenebilir hâle getirecekler
Gereksinimler
- PHP 8.0+ · openssl eklentisi (zorunlu) · MySQL 5.7+ / MariaDB 10.3+ · pdo_mysql · mbstring · HTTPS (localhost hariç)
Nasıl Kurulur?
- Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
- database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
- .env.example dosyasını .env adıyla kopyalayın
- php bin/vapid-keys.php çalıştırıp açık anahtarı .env içine yapıştırın
- HTTPS üzerinden açın (localhost tek istisnadır) ve bildirimleri açın
Veritabanı şeması projedeki database.sql dosyasında.
Nasıl Çalışıyor?
PWA ve Web Push Bildirim Sistemi
PHP 8 · PDO · MySQL · Service Worker · VAPID · aes128gcm · Çevrimdışı Çalışma · Çılgın Yazılım Tasarım Kalıbı
Tarayıcı kapalıyken bile bildirim gönderin — kütüphane kullanmadan, saf PHP ile.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemo hesapları
| Rol | E-posta | Parola |
|---|---|---|
| Yönetici | [email protected] | Admin1234 |
| Kullanıcı | [email protected] | Demo1234 |
Demoda 60 saniyede neleri deneyebilirsiniz?
| # | Şunu deneyin | Perde arkasında ne oluyor? |
|---|---|---|
| 1 | Push Bildirimler sayfasında "Bildirimleri aç" deyin ve tarayıcı iznini verin | Tarayıcı push servisinden (Google/Mozilla/Apple) size özel bir abonelik adresi alır ve sunucuya kaydeder. Bu adres cihaza özeldir; sunucu üretemez |
| 2 | "Kendi cihazlarıma gönder" düğmesine basın | PHP, VAPID ile imzalı ve uçtan uca şifreli bir istek hazırlayıp push servisine yollar. Bildirim tarayıcınıza servis üzerinden ulaşır |
| 3 | Bildirimi gönderdikten sonra sekmeyi kapatın ve tekrar gönderin | Bildirim yine gelir. Onu gösteren şey sayfa değil, arka planda çalışan service worker'dır |
| 4 | Gelen bildirime tıklayın | Uygulama açılır. Zaten açık bir sekme varsa yenisi açılmaz, var olan sekme öne getirilir (clients.matchAll) |
| 5 | Kayıtlı Cihazlar tablosuna bakın | Cihaz adı, kullanıcı ve push servisi (fcm.googleapis.com, updates.push.services.mozilla.com, web.push.apple.com…) ayrı ayrı görünür |
| 6 | Son satırdaki "3 hata" rozetine bakın | O aboneliğe üç kez geçici hata alındı. Kesin ölüm (404/410) olsaydı satır zaten silinmiş olurdu; sayaç, ikisini ayırt etmek içindir |
| 7 | Tarayıcının çevrimdışı modunu açıp sayfayı yenileyin | Beyaz hata sayfası yerine offline.html gelir. Service worker gezinme isteklerine ağ yokken bu sayfayla yanıt verir |
| 8 | Adres çubuğundaki "Uygulamayı yükle" simgesine basın | Uygulama ana ekrana eklenir ve adres çubuğu olmadan açılır (display: standalone) |
| 9 | Kontrol panelindeki PWA Kurulum Durumu kartına bakın | Service worker kapsamı, manifest, VAPID açık anahtarının tanımlı olup olmadığı ve çevrimdışı sayfası tek bakışta |
| 10 | Telefonunuzdan açın | Alt navigasyon devreye girer; sayfa gövdesinde yatay kaydırma yoktur |
Neden localhost'ta çalışıyor amahttp://bir sunucuda çalışmıyor? Service worker ve push güvenli bağlam ister. Tarayıcılarlocalhost'u tek istisna olarak güvenli sayar; başka her yerde HTTPS zorunludur.
Demo alanı hakkında bilinmesi gerekenler
| Konu | Durum |
|---|---|
| Veriler | database.sql içindeki 51 kullanıcı + 4 örnek abonelik. Gerçek kişi verisi yoktur. |
| Örnek abonelikler | Başka kullanıcılara aittir (#3, #7, #12, #20). "Kendi cihazlarıma gönder" yalnızca sizin kendi tarayıcınızdan oluşturduğunuz aboneliğe gönderir; örnek satırlar gönderiminizi etkilemez. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç hâline döner; aboneliğiniz kalıcı değildir. |
| Bildirim izni | Tarayıcınız soracaktır. Reddederseniz sayfa çalışmaya devam eder, yalnızca gönderim yapılamaz. |
APP_DEBUG | Canlıda kendiliğinden false — sunucu adından türetilir. |
| Bağımlılık | Sıfır. web-push-php yok, Composer yok, npm yok, CDN yok. VAPID imzası ve şifreleme saf PHP'de. |
Bu Proje Nedir?
Web push, ilk bakışta "bir istek at, bildirim gitsin" gibi görünür. Gerçekte üç ayrı parçanın uyum içinde çalışması gerekir ve üçü de tarayıcı tarafından zorunlu kılınmıştır:
- Service worker — sayfadan bağımsız çalışan bir betik. Bildirimi gösteren odur; sekme kapalıyken bile ayaktadır.
- VAPID imzası — push servisi (Google, Mozilla, Apple) size ait olmayan bir sunucudur ve "bu isteği gerçekten o site mi yolladı?" diye sorar. Cevabı ES256 ile imzalanmış bir JWT verir.
- Uçtan uca şifreleme — push servisi mesajın içeriğini okuyamaz. İçerik, tarayıcının abonelikte verdiği anahtarlarla şifrelenir; sunucu ile tarayıcı arasında araya giren servis yalnızca şifreli baytları taşır.
Çoğu PHP örneği bu üçünü composer require minishlink/web-push ile geçiştirir. Bu proje hiçbir kütüphane kullanmadan yazıyor: WebPush sınıfı VAPID JWT'sini üretip ES256 ile imzalar, ECDH anahtar değişimini ve HKDF türetmesini yapar, içeriği aes128gcm (RFC 8188) ile şifreler ve isteği kendisi gönderir.
Sonuç: nasıl çalıştığını gerçekten görebileceğiniz, üretime hazır ve bağımlılıksız bir uygulama — üstelik çevrimdışı çalışan bir PWA kabuğuyla birlikte.
Kimler için uygun?
- Sitesine bildirim eklemek isteyip "FCM SDK'sı olmadan olur mu?" diye soranlar
- VAPID'in ne olduğunu ve neden gerektiğini öğrenmek isteyenler
- Web push şifrelemesinin adım adım nasıl yapıldığını görmek isteyenler
- Uygulamasını PWA'ya çevirip ana ekrana eklenebilir hâle getirecekler
- Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir panel kalıbı arayanlar
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
İçindekiler
- Canlı Demo
- Bu Proje Nedir?
- Ekran Görüntüleri
- Bir bildirimin yolculuğu
- Kritik Kararlar
- Neler Var?
- Service Worker
- VAPID anahtarı üretmek
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Dosya Yapısı
- Veritabanı Şeması
- SSS
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
Ekran Görüntüleri
Push bildirimler
Projenin konusu bu sayfadadır. "Bildirimleri aç" düğmesi tarayıcıdan izin ister ve push servisinden (Google, Mozilla, Apple) cihaza özel bir abonelik adresi alıp sunucuya kaydeder — o adresi sunucu üretemez. Kayıtlı Cihazlar tablosu cihaz adını, kullanıcıyı ve hangi push servisinin kullanıldığını ayrı ayrı gösterir. Satırdaki hata rozeti geçici hataları sayar; kesin ölüm (404/410) olsaydı satır zaten silinmiş olurdu.

Kontrol paneli
PWA Kurulum Durumu kartı dört şeyi tek bakışta verir: service worker kapsamı, manifest, VAPID açık anahtarının tanımlı olup olmadığı ve çevrimdışı sayfası. Bir bildirimin gitmemesinin sebebi neredeyse her zaman bu dörtten biridir.

Giriş ekranı
Demo hesapları tek tıkla doldurulur. Giriş denemeleri hız sınırına tabidir; art arda başarısız denemeden sonra hesap geçici olarak kilitlenir.

Koyu tema
Tema tarayıcıda değil kullanıcı hesabında saklanır; başka bir cihazdan girdiğinizde de aynı gelir.

Mobil görünüm
390px genişlikte alt navigasyon devreye girer. Sayfa gövdesinde yatay kaydırma yoktur. Uygulama ana ekrana eklendiğinde adres çubuğu olmadan açılır (display: standalone).
Bir bildirimin yolculuğu
ABONE OLMA (bir kez)
────────────────────
Tarayıcı Sunucu (PHP)
│ "Bildirimleri aç" │
│ Notification.requestPermission()│
│ │
│ pushManager.subscribe({ │
│ applicationServerKey: VAPID_PUBLIC })
│ │ │
│ ▼ │
│ PUSH SERVİSİ (Google/Mozilla/Apple)
│ │ │
│ endpoint + p256dh + auth │
│ │ │
│ └──── POST /api/push/subscribe ──► push_subscriptions
│ (kullanıcıya bağlı)
GÖNDERME (her seferinde)
────────────────────────
Sunucu (PHP) Tarayıcı
│
│ 1) VAPID JWT üret: {aud, exp, sub}
│ ES256 ile imzala (P-256 özel anahtar)
│
│ 2) İçeriği ŞİFRELE (aes128gcm / RFC 8188)
│ - geçici ECDH anahtar çifti üret
│ - abonenin p256dh'si ile ortak sır türet
│ - HKDF ile içerik anahtarı + nonce
│ - AES-128-GCM ile şifrele
│
│ 3) POST <endpoint>
│ Authorization: vapid t=<jwt>, k=<public>
│ Content-Encoding: aes128gcm
│ TTL: 86400
│ │
│ ▼
│ PUSH SERVİSİ ── içeriği OKUYAMAZ, sadece taşır
│ │
│ ▼
│ service-worker.js 'push' olayı
│ │ event.data.json()
│ ▼
│ showNotification(title, {body, icon, data})
│ │
│ ▼ kullanıcı tıklar
│ 'notificationclick' → açık sekme varsa ÖNE GETİR,
│ yoksa yeni sekme aç
│
└── Yanıt 404/410 ise → failures++ (ölü abonelik ayıklanır)
Kritik Kararlar
1. Hiçbir push kütüphanesi kullanılmadı
web-push-php iyi bir kütüphanedir ve üretimde kullanmakta hiçbir sakınca yoktur. Ama bu proje öğretmek için yazıldı: kütüphane çağrısı, VAPID'in ne olduğunu ve şifrelemenin neden gerektiğini gizler.
app/Core/WebPush.php işi baştan sona yapar:
| Adım | Nasıl |
|---|---|
| VAPID JWT | {aud, exp, sub} → base64url → openssl_sign (ES256) → DER imzayı ham 64 bayta çevir |
| Ortak sır | Geçici ECDH anahtar çifti + abonenin p256dh açık anahtarı |
| Anahtar türetme | HKDF ile içerik anahtarı (16 bayt) ve nonce (12 bayt) |
| Şifreleme | openssl_encrypt · AES-128-GCM · RFC 8188 gövde biçimi |
Bu, PHP'nin openssl eklentisi dışında hiçbir şey gerektirmez.
2. aes128gcm — eski aesgcm değil
Web push'un iki şifreleme biçimi vardır. Eski aesgcm, tuz ve geçici anahtarı ayrı başlıklarda (Encryption, Crypto-Key) taşır ve uygulaması hem karmaşık hem hataya açıktır.
aes128gcm (RFC 8188) ikisini de gövdenin içine koyar. Sonuç: daha az başlık, daha az yanlış yapma ihtimali. Bütün güncel tarayıcılar destekler.
'Content-Encoding: aes128gcm',
3. VAPID özel anahtarı .env'de değil, dosyada
Açık anahtar .env içinde durur — zaten JavaScript'e gönderilir, gizli değildir.
Özel anahtar ise PEM dosyası olarak storage/vapid.pem içinde tutulur ve o klasör web'e kapalıdır. Sebep: PEM çok satırlıdır ve .env biçimi tek satır bekler; sıkıştırmaya çalışmak sessiz bozulmalara yol açar.
Anahtar çiftini kaybederseniz bütün abonelikler geçersiz olur — abonelikler açık anahtara bağlıdır. Kullanıcılar yeniden izin vermek zorunda kalır. storage/vapid.pem dosyasını yedekleyin.
4. Bildirim yalnızca giriş yapan kullanıcının kendi cihazlarına gider
// Abonelikler kullanıcıya bağlıdır; gönderim de öyle.
WHERE user_id = :id"Herkese gönder" düğmesi bilerek yoktur. Bir demo uygulamasında herkese bildirim gönderebilen bir düğme, ilk ziyaretçinin oyuncağı olurdu.
Kendi projenizde toplu gönderim yaparken sırayla değil, kuyrukla gönderin: 10.000 aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter.
5. Ölü abonelik ile geçici hata ayrı ele alınır
Kullanıcı tarayıcı verilerini temizlerse veya uygulamayı kaldırırsa abonelik geçersiz olur ama sunucu bunu ancak gönderim denediğinde öğrenir. Push servisinin yanıt kodu ikisini birbirinden ayırır:
// app/Http/Controllers/Api/PushApiController.php
if (in_array($result['status'], [404, 410], true)) {
$repository->deleteByEndpoint($subscription['endpoint']); // kesin ölüm
} else {
$repository->markFailure($subscription['endpoint']); // geçici olabilir
}404 / 410 kesin ölümdür: push servisi "böyle bir adres yok" diyor. Satır hemen silinir; beklemenin bir anlamı yoktur.
Diğer hata kodları (500, ağ zaman aşımı) geçici olabilir. Bunlarda satır silinmez, yalnızca failures sayacı artar. Tek bir geçici hata yüzünden çalışan bir aboneliği atmak, kullanıcıyı sessizce bildirimsiz bırakırdı.
Abonelik yeniden kaydedildiğinde sayaç sıfırlanır — ON DUPLICATE KEY UPDATE ... failures = 0.
6. Service worker fetch olayında gezinme ile varlık isteğini ayırır
Her isteği önbelleğe almak cazip görünür ama tehlikelidir: eski bir JavaScript dosyası önbellekte kalırsa uygulama açıklanamaz biçimde bozulur.
Bu yüzden service worker yalnızca gezinme isteklerinde (request.mode === 'navigate') devreye girer: ağ varsa ağdan, yoksa offline.html'den yanıt verir. Diğer istekler doğrudan ağa gider.
7. notificationclick yeni sekme açmaz — var olanı öne getirir
clients.matchAll({ type: 'window', includeUncontrolled: true })Bildirime her tıklandığında yeni sekme açan bir uygulama, gün sonunda 14 sekme bırakır. Açık bir sekme varsa
focus() edilir; yoksa yenisi açılır.
8. Manifest start_url olarak dashboard verir
Ana ekrana eklenen uygulama açıldığında giriş sayfasını değil, panele gider. Oturum yoksa uygulama zaten girişe yönlendirir; ama oturum varsa kullanıcı doğrudan işine başlar.
Neler Var?
Web Push (saf PHP)
- VAPID JWT üretimi ve ES256 imzası
- DER → ham imza dönüşümü
- ECDH ortak sır + HKDF anahtar türetme
- aes128gcm (RFC 8188) içerik şifreleme
TTL,Content-EncodingveAuthorization: vapidbaşlıkları404/410'da silme, geçici hatada sayaç- Anahtar üretici CLI (
bin/vapid-keys.php)
PWA kabuğu
service-worker.js— install · activate · fetch · push · notificationclick · pushsubscriptionchangemanifest.webmanifest— ana ekrana eklenebiliroffline.html— ağ yokken gezinme yanıtı
Panel
- Bildirimleri aç / kapat (izin akışıyla)
- Kendi cihazlarına test bildirimi gönderme
- Kayıtlı cihazlar: ad, kullanıcı, push servisi, hata sayacı
- PWA kurulum durumu kartı (worker · manifest · VAPID · çevrimdışı)
Ortak altyapı
- Oturum girişi, "beni hatırla", hız sınırı, CSRF
- CSP,
X-Frame-Options: DENY,nosniff - Açık / koyu tema, hesaba kayıtlı
- Mobilde alt navigasyon, yatay kaydırma yok
- Kullanıcılar sayfasında canlı filtre (JS kapalıysa da çalışır)
- Sıfır bağımlılık
Service Worker
Service worker, sayfadan bağımsız çalışan bir betiktir. Sekme kapalıyken bile tarayıcı onu uyandırıp bir olay verebilir — push bildiriminin sekme kapalıyken gelmesinin sebebi budur.
Altı olay dinlenir:
| Olay | Ne yapar |
|---|---|
install | Çevrimdışı kabuğu önbelleğe alır (offline.html, logo) |
activate | Eski sürümün önbelleklerini siler — bayat dosya kalmaz |
fetch | Yalnızca gezinme isteklerinde: ağ yoksa offline.html |
push | Gelen veriyi ayrıştırır ve bildirimi gösterir |
notificationclick | Açık sekmeyi öne getirir, yoksa yenisini açar |
pushsubscriptionchange | Tarayıcı aboneliği yenilerse sunucuya haber verir |
Kapsam (scope) kuralı: service worker yalnızca kendi klasörü ve altındaki adresleri kontrol edebilir. Bu yüzden service-worker.js proje kökündedir; assets/js/ altına koysaydınız yalnızca o klasörü kontrol ederdi ve gezinme isteklerini hiç göremezdi.
Güncelleme: tarayıcı dosyayı bayt bayt karşılaştırır. Tek bir karakter değişirse yeni sürümü kurar. Bu yüzden dosyanın başındaki önbellek sürüm adını (var CACHE = 'cy-pwa-v1') her değişiklikte artırın; activate olayı eskileri temizler.
VAPID anahtarı üretmek
VAPID, "bu bildirimi gerçekten o site gönderdi" diyebilmek için kullanılan bir anahtar çiftidir. Bir kez üretilir ve değişmez.
php bin/vapid-keys.phpBetik P-256 (
prime256v1) eğrisinde bir ECDSA çifti üretir:
- Özel anahtar →
storage/vapid.pem(web'e kapalı klasör) - Açık anahtar → ekrana yazılır;
.enviçindekiVAPID_PUBLIC_KEYsatırına yapıştırın
VAPID_PUBLIC_KEY=BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQ...
VAPID_SUBJECT=mailto:[email protected]VAPID_SUBJECT bir mailto: veya https: adresidir; push servisi bir sorun olduğunda kime ulaşacağını buradan öğrenir.
Windows/XAMPP'taopenssl.cnfbulunamıyor hatası alırsanız betik gerekli seçenekleriWebPush::keygenOptions()üzerinden zaten veriyor; yine de sorun sürersephp.iniiçindekiopenssl.cafileveOPENSSL_CONFortam değişkenini kontrol edin.
Anahtarı değiştirmeyin. Abonelikler açık anahtara bağlıdır; değiştirirseniz mevcut bütün abonelikler geçersiz olur ve kullanıcılar yeniden izin vermek zorunda kalır.
Güvenlik: Neyi, Nasıl Kapattık?
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| Özel anahtar sızıntısı | PEM'i assets/ altına koymak | storage/vapid.pem — klasör .htaccess ile web'e kapalı |
| İçeriğin push servisinde okunması | Şifresiz gönderim | aes128gcm ile uçtan uca şifreleme; servis yalnızca şifreli baytları taşır |
| Sahte gönderim | İmzasız istek | VAPID / ES256 imzası; push servisi imzasız isteği reddeder |
| Başkasının aboneliğini silme (IDOR) | DELETE WHERE endpoint = ? | Sorguya user_id de katılır; kullanıcı yalnızca kendi aboneliğini silebilir |
| Herkese bildirim gönderme | Kısıtsız "toplu gönder" | Gönderim yalnızca giriş yapan kullanıcının kendi cihazlarına |
| Ölü aboneliklerin birikmesi | Hiç temizlememek | 404/410 yanıtında satır hemen silinir; geçici hatalarda yalnızca sayaç artar |
| CLI betiğine web'den erişim | bin/ klasörü açık | bin/.htaccess ile kapalı ve betikte PHP_SAPI === 'cli' kontrolü |
| SQL enjeksiyonu | "... WHERE endpoint = '$e'" | Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false |
| CSRF | Gizli alan yok | Her POST'ta jeton; hash_equals() |
| XSS | echo $row['user_agent'] | Sunucuda e(); ayrıca CSP script-src 'self' |
| Hata sızıntısı | Canlıda display_errors=On | APP_DEBUG ortamdan türetilir |
| Güvensiz bağlam | http:// üzerinden push denemek | Tarayıcı zaten engeller; belgeler HTTPS'i zorunlu olarak işaretler |
Kurulum
Gereksinimler
| PHP | 8.0 veya üzeri · openssl eklentisi zorunlu |
| MySQL / MariaDB | 5.7+ / 10.3+ |
| Web sunucusu | Apache (mod_rewrite) veya Nginx |
| Bağlantı | HTTPS (yalnızca localhost istisnadır) |
Adımlar
git clone https://github.com/CilginYazilim/pwa-push-notification-system.git
cd pwa-push-notification-system
mysql -u root -p < database.sql
cp .env.example .env # Windows: copy .env.example .env
php bin/vapid-keys.php # açık anahtarı .env'e yapıştırınAçın:
http://localhost/pwa-push-notification-system/ · Giriş: [email protected] / Admin1234
Sonra Push Bildirimler sayfasından "Bildirimleri aç" deyin.
Yapılandırma
APP_DEBUG=true # silerseniz: yerelde açık, canlıda kapalı
APP_URL=
APP_PRETTY_URLS=true
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=cy_pwa_push
DB_USER=root
DB_PASS=
# --- WEB PUSH (VAPID) ---
VAPID_PUBLIC_KEY= # bin/vapid-keys.php çıktısı
VAPID_SUBJECT=mailto:[email protected]Özel anahtar
.env'de değildir; storage/vapid.pem dosyasındadır.
| Değer | Yeri | Varsayılan | Ne yapar |
|---|---|---|---|
WebPush::TTL | app/Core/WebPush.php | 86400 | Push servisi mesajı kaç saniye saklasın (cihaz kapalıysa) |
| Şifreleme | WebPush::send() | aes128gcm | RFC 8188 içerik şifreleme biçimi |
CACHE | service-worker.js | 'cy-pwa-v1' | Önbellek sürüm adı; değiştirince activate eskileri temizler |
Dosya Yapısı
pwa-push-notification-system/
│
├── index.php Ön denetleyici — TEK giriş noktası
├── service-worker.js ★ KÖKTE olmalı (kapsam kuralı)
├── manifest.webmanifest Ana ekrana ekleme künyesi
├── offline.html Ağ yokken gösterilen sayfa
├── database.sql Şema + 51 kullanıcı + 4 örnek abonelik
├── .env.example
│
├── bin/
│ ├── vapid-keys.php ★ P-256 anahtar çifti üretici (CLI)
│ └── .htaccess Web erişimine kapalı
│
├── storage/
│ ├── vapid.pem ★ VAPID ÖZEL ANAHTARI — web'e kapalı
│ └── .htaccess
│
├── app/
│ ├── Core/
│ │ ├── WebPush.php ★ VAPID JWT · ECDH · HKDF · aes128gcm · gönderim
│ │ ├── Auth.php · Session.php · Csrf.php · RateLimiter.php
│ │ ├── Database.php PDO (EMULATE_PREPARES = false)
│ │ ├── Env.php .env okuyucu + isLocalHost()
│ │ └── ...
│ │
│ ├── Http/Controllers/ Auth · Dashboard · Push · User · Api
│ ├── Repositories/PushSubscriptionRepository.php
│ └── Support/helpers.php
│
├── views/ Düzenler, push sayfası
├── assets/ css · js (push izin akışı) · images
├── config/config.php
├── routes/web.php
└── docs/screenshots/
Veritabanı Şeması
push_subscriptions
| Sütun | Tip | İşi |
|---|---|---|
id | INT UNSIGNED | Birincil anahtar |
user_id | INT UNSIGNED | Abonelik kime ait (ON DELETE CASCADE) |
endpoint | VARCHAR(500) | Push servisinin verdiği cihaza özel adres |
p256dh | VARCHAR(255) | Tarayıcının açık anahtarı — şifreleme bununla yapılır |
auth | VARCHAR(255) | Kimlik doğrulama sırrı (şifrelemenin ikinci girdisi) |
user_agent | VARCHAR(255) | "Hangi cihazım bu?" sorusuna cevap verecek kadarı |
failures | SMALLINT UNSIGNED | Geçici hata sayacı; 404/410 gelirse satır zaten silinir |
created_at · updated_at | DATETIME | Abone olma ve son yenilenme anı |
| Karar | Neden |
|---|---|
endpoint benzersiz | Aynı tarayıcı iki kez abone olursa yeni satır değil, var olan güncellenir; failures de sıfırlanır |
Anahtarlar (p256dh, auth) saklanıyor | Şifreleme her gönderimde bu iki değerle yapılır; onlarsız mesaj hazırlanamaz |
user_agent tam saklanmıyor | Cihazı tanımaya yetecek kadarı yeterli; parmak izi çıkarmaya gerek yok |
failures yalnızca geçici hatalar için | 404/410 kesin ölümdür ve hemen silinir; ama geçici bir ağ hatası yüzünden çalışan bir aboneliği atmak kullanıcıyı sessizce bildirimsiz bırakırdı |
Kullanıcıya bağlı (CASCADE) | Hesap silinince abonelikleri de gider; ölü kayıt kalmaz |
SSS
HTTPS olmadan çalışır mı?
Hayır — localhost dışında çalışmaz ve bu bir tercih değil, tarayıcı kuralıdır.
Service worker ve push API'si güvenli bağlam ister. Sebebi açıktır: service worker sitenizin bütün isteklerini görebilir; araya giren biri onu enjekte edebilseydi tam kontrolü ele geçirirdi.
Geliştirme için localhost yeterlidir. Yerel ağdaki bir telefondan denemek isterseniz ngrok gibi bir tünel veya kendi imzaladığınız bir sertifika kullanın.
iOS'ta çalışıyor mu?
Evet, iOS 16.4 ve sonrasında — ama bir şartla: kullanıcının uygulamayı önce ana ekrana eklemesi gerekir. Safari sekmesinde açık dururken push izni istenemez.
Bu yüzden manifest ve "ana ekrana ekle" akışı yalnızca bir süs değil, iOS'ta push'un ön koşuludur.
Bildirim gitmiyor, hata da yok
Sırayla kontrol edin:
- HTTPS mi? (
localhosthariç) - Tarayıcı izni verilmiş mi? Adres çubuğundaki kilit simgesinden bakın; bir kez reddedildiyse sayfa bir daha soramaz, izni elle açmanız gerekir.
.enviçindeVAPID_PUBLIC_KEYdolu mu? Kontrol panelindeki kart size söyler.storage/vapid.pemdosyası var mı ve PHP onu okuyabiliyor mu?- İşletim sistemi düzeyinde bildirimler kapalı olabilir (Windows'ta "Odak yardımı", macOS'ta "Rahatsız etmeyin").
Toplu bildirim nasıl gönderirim?
Döngüyle değil, kuyrukla. 10.000 aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter ve yarısı gitmiş, yarısı gitmemiş bir gönderim bırakır.
Doğru kalıp: her abonelik için kuyruğa bir iş bırakın, worker'lar paralel göndersin. Kuyruk örneği için İş Kuyruğu ve Worker Sistemi'ne bakın.
Neden web-push-php kullanmadınız?
Üretimde kullanabilirsiniz, iyi bir kütüphanedir. Ama bu proje VAPID'in ve şifrelemenin nasıl çalıştığını göstermek için yazıldı; tek satırlık bir kütüphane çağrısı bunu gizlerdi.
WebPush.php 471 satırdır ve her adımı yorumlarla açıklar. Okuduktan sonra kütüphaneye geçmek isterseniz, artık onun ne yaptığını biliyor olacaksınız.
Service worker'ı güncelledim ama eski sürüm çalışıyor
Tarayıcı yeni service worker'ı kurar ama eski sürüm hâlâ bir sekmeyi kontrol ediyorsa beklemeye alır.
Geliştirme sırasında: DevTools → Application → Service Workers → "Update on reload" işaretleyin veya "Unregister" deyip sayfayı yenileyin.
Üretimde: CACHE değerini her sürümde artırın (cy-pwa-v2 …); activate olayı eski önbellekleri temizler.
Canlı Ortama Alırken
- [ ] HTTPS zorunlu olsun — push ve service worker bunsuz çalışmaz
- [ ]
.enviçindeAPP_DEBUG=false(veya satırı tümüyle silin) - [ ]
VAPID_PUBLIC_KEYdolu,storage/vapid.pemyerinde - [ ]
storage/vapid.pemdosyasını yedekleyin — kaybederseniz tüm abonelikler geçersiz olur - [ ]
storage/vebin/klasörlerinin web'den erişilemediğini doğrulayın (403) - [ ]
service-worker.jsproje kökünde mi? - [ ]
manifest.webmanifestiçindekistart_urlve simgeler doğru mu? - [ ]
VAPID_SUBJECTgerçek bir iletişim adresi olsun - [ ] Toplu gönderim yapacaksanız kuyruk kullanın
- [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin
Sorun Giderme
| Belirti | Sebep | Çözüm |
|---|---|---|
| "Bildirimleri aç" hiçbir şey yapmıyor | HTTPS yok | localhost kullanın veya sertifika kurun |
| İzin bir daha sorulmuyor | Kullanıcı bir kez reddetti | Site ayarlarından elle açın |
Gönderim 401 / 403 dönüyor | VAPID imzası geçersiz | VAPID_PUBLIC_KEY ile storage/vapid.pem aynı çiftten mi? |
Gönderim 404 / 410 dönüyor | Abonelik ölmüş | Normaldir; failures artar, eşik aşılınca silinir |
| Bildirim geliyor ama içerik boş | Şifreleme adımı başarısız | openssl eklentisi açık mı? |
openssl.cnf bulunamadı (Windows) | XAMPP yapılandırması | OPENSSL_CONF ortam değişkenini ayarlayın |
| Service worker güncellenmiyor | Eski sürüm sekmeyi kontrol ediyor | CACHE değerini artırın; DevTools'tan "Unregister" |
| Çevrimdışıyken beyaz sayfa | Service worker kayıtlı değil | DevTools → Application → Service Workers'tan durumu kontrol edin |
| iOS'ta izin istenmiyor | Ana ekrana eklenmemiş | Paylaş → "Ana Ekrana Ekle" |
Yol Haritası
- [ ] Bildirim eylem düğmeleri (
actions) örneği - [ ] Toplu gönderim için kuyruk entegrasyonu
- [ ] Gönderim geçmişi tablosu (kime, ne zaman, sonuç)
- [ ] Konu bazlı abonelik (topic) desteği
- [ ]
Urgencybaşlığı desteği ve panelden seçilebilmesi
Katkı
Hata bildirimi ve öneriler için issue açabilirsiniz.
Lisans
MIT — ticari projelerinizde de özgürce kullanabilirsiniz.
Kaynak Kod soldaki ağaçtan bir dosya seçin
-
app
-
Core
- Auth.php 13.9 KB
- Autoloader.php 2.2 KB
- Config.php 1.9 KB
- Csrf.php 2.8 KB
- Database.php 3.5 KB
- Env.php 4.3 KB
- Flash.php 2.6 KB
- Middleware.php 2.5 KB
- Paginator.php 8.6 KB
- RateLimiter.php 3.9 KB
- Request.php 4.5 KB
- Response.php 7 KB
- Router.php 6.3 KB
- Session.php 6.3 KB
- Validator.php 8.8 KB
- View.php 3.5 KB
- WebPush.php 17.5 KB
-
Http
-
Controllers
-
Api
- PreferenceApiController.php 2.5 KB
- PushApiController.php 6.8 KB
- AuthController.php 3.2 KB
- DashboardController.php 1.4 KB
- PushController.php 1.9 KB
- UserController.php 3.1 KB
-
- Controller.php 1.3 KB
-
-
Models
- User.php 5.9 KB
-
Repositories
- ActivityRepository.php 3.8 KB
- PushSubscriptionRepository.php 5.5 KB
- RememberTokenRepository.php 5.8 KB
- UserRepository.php 6.9 KB
-
Support
- helpers.php 12.3 KB
- .htaccess 851 B
-
-
assets
-
css
- admin.css 58.6 KB
- bootstrap.min.css 227.5 KB
- cilginyazilim.css 29.8 KB
- feature.css 2.1 KB
-
images
- logo.png 70.4 KB
-
js
- app.js 20.2 KB
- bootstrap.bundle.js 203.2 KB
- jquery-3.7.0.js 278.3 KB
- login.js 1.8 KB
- push.js 10.5 KB
- users.js 4.7 KB
-
-
bin
- .htaccess 666 B
- vapid-keys.php 4.4 KB
-
config
- .htaccess 196 B
- config.php 5 KB
- menu.php 1 KB
-
docs
-
screenshots
- 01-giris.png 377 KB
- 02-kontrol-paneli.png 69.6 KB
- 03-push-bildirimler.png 84 KB
- 04-koyu-tema.png 83.8 KB
- 05-mobil.png 55 KB
-
-
routes
- .htaccess 197 B
- web.php 3.3 KB
-
storage
- .gitkeep 1 B
- .htaccess 685 B
- vapid.pem 241 B
-
views
-
auth
- login.php 7.4 KB
-
dashboard
- _feature.php 3.3 KB
- index.php 4 KB
-
errors
- 404.php 844 B
- 500.php 565 B
-
layouts
- admin.php 6.8 KB
- auth.php 2.9 KB
- plain.php 1.3 KB
-
partials
- bottomnav.php 2.1 KB
- head_extra.php 1.3 KB
- pagination.php 4.9 KB
- sidebar.php 2.7 KB
- topbar.php 3.5 KB
-
push
- index.php 8.1 KB
-
users
- index.php 6.8 KB
- .htaccess 262 B
-
- .gitattributes 2.3 KB
- .gitignore 1 KB
- .htaccess 7.2 KB
- CHANGELOG.md 7.6 KB
- database.sql 19.2 KB
- index.php 5.4 KB
- LICENSE 1.1 KB
- manifest.webmanifest 703 B
- offline.html 3.9 KB
- README.en.md 30.5 KB
- README.md 31.4 KB
- service-worker.js 9.3 KB
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli
değerler gösterilmeden önce maskelenir (••••••••).
Sık Sorulan Sorular
Hayır, localhost dışında çalışmaz ve bu bir tercih değil tarayıcı kuralıdır. Service worker ve push API'si güvenli bağlam ister; sebebi açıktır, service worker sitenizin bütün isteklerini görebilir ve araya giren biri onu enjekte edebilseydi tam kontrolü ele geçirirdi. Geliştirme için localhost yeterlidir.
Evet, iOS 16.4 ve sonrasında — ama bir şartla: kullanıcının uygulamayı önce ana ekrana eklemesi gerekir. Safari sekmesinde açık dururken push izni istenemez. Bu yüzden manifest ve "ana ekrana ekle" akışı yalnızca bir süs değil, iOS'ta push'un ön koşuludur.
Üretimde kullanabilirsiniz, iyi bir kütüphanedir. Ama bu proje VAPID'in ve şifrelemenin nasıl çalıştığını göstermek için yazıldı; tek satırlık bir kütüphane çağrısı bunu gizlerdi. WebPush.php 471 satırdır ve her adımı yorumlarla açıklar.
Döngüyle değil, kuyrukla. On bin aboneye tek istekte gönderim yapmaya çalışmak PHP'nin zaman aşımına takılmasıyla biter ve yarısı gitmiş bir gönderim bırakır. Doğru kalıp: her abonelik için kuyruğa bir iş bırakın, worker'lar paralel göndersin.
Değiştirmeyin. Abonelikler açık anahtara bağlıdır; değiştirirseniz mevcut bütün abonelikler geçersiz olur ve kullanıcılar yeniden izin vermek zorunda kalır. storage/vapid.pem dosyasını mutlaka yedekleyin.
Sırayla: HTTPS mi (localhost hariç), tarayıcı izni verilmiş mi (bir kez reddedildiyse sayfa bir daha soramaz), .env içinde VAPID_PUBLIC_KEY dolu mu, storage/vapid.pem var mı ve PHP okuyabiliyor mu, işletim sistemi düzeyinde bildirimler kapalı mı. Kontrol panelindeki PWA kurulum kartı ilk üçünü size söyler.
Yorumlar 0 konuşma
Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.
Soru Sor veya Yorum Yaz
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.