---
title: "PWA ve Web Push Bildirim Sistemi"
url: "https://cilginyazilim.com/kutuphane/php-pwa-push-notification"
description: "Bu Ã¶rnek, PHP 8 ile web push bildirimini hiÃ§bir kÃ¼tÃ¼phane kullanmadan uÃ§tan uca gerÃ§ekleÅŸtirir ve bunu Ã§evrimdÄ±ÅŸÄ± Ã§alÄ±ÅŸabilen bir PWA kabuÄŸuyla birleÅŸtirir. Web push Ã¼Ã§ zorunlu parÃ§adan oluÅŸur: sayfadan baÄŸÄ±msÄ±z Ã§alÄ±ÅŸan bir service worker, push servisine kimliÄŸi kanÄ±tlayan VAPID imzasÄ± ve iÃ§eriÄŸi push servisinin okuyamayacaÄŸÄ± biÃ§imde koruyan uÃ§tan uca ÅŸifreleme. Projedeki WebPush sÄ±nÄ±fÄ± Ã¼Ã§Ã¼nÃ¼ de kendisi yapar: VAPID JWT'sini kurup ES256 ile imzalar ve OpenSSL'in Ã¼rettiÄŸi DER imzayÄ± ham 64 bayta Ã§evirir, geÃ§ici bir ECDH anahtar Ã§ifti Ã¼retip abonenin p256dh anahtarÄ±yla ortak sÄ±r tÃ¼retir, HKDF ile iÃ§erik anahtarÄ± ve nonce Ã¼retir ve iÃ§eriÄŸi aes128gcm (RFC 8188) biÃ§iminde ÅŸifreler. Eski aesgcm biÃ§imi yerine aes128gcm seÃ§ilmiÅŸtir Ã§Ã¼nkÃ¼ tuz ve geÃ§ici anahtarÄ± ayrÄ± baÅŸlÄ±klar yerine gÃ¶vdenin iÃ§inde taÅŸÄ±r. Service worker install, activate, fetch, push, notificationclick ve pushsubscriptionchange olaylarÄ±nÄ± dinler; fetch yalnÄ±zca gezinme isteklerinde devreye girer, bÃ¶ylece bayat bir JavaScript dosyasÄ± Ã¶nbellekte kalmaz. Ã–lÃ¼ abonelikler 404/410 yanÄ±tÄ±nda hemen silinir, geÃ§ici hatalarda yalnÄ±zca sayaÃ§ artar."
published: "2026-09-03T09:44:21+03:00"
modified: "2026-09-05T02:24:55+03:00"
category: "Uygulama Ä°skeleti"
type: "kod Ã¶rneÄŸi"
difficulty: "Ä°leri"
tech: ["PHP 8", "PDO", "MySQL", "Bootstrap 5", "Ajax", "Oturum GiriÅŸi"]
tags: ["Bildirim", "Ã‡evrimdÄ±ÅŸÄ±", "PWA", "Service Worker", "VAPID", "Web Push"]
license: "MIT"
site: "CÄ±lgÄ±nYazÄ±lÄ±m"
language: "tr"
---

# PWA ve Web Push Bildirim Sistemi

Bu Ã¶rnek, PHP 8 ile web push bildirimini hiÃ§bir kÃ¼tÃ¼phane kullanmadan uÃ§tan uca gerÃ§ekleÅŸtirir ve bunu Ã§evrimdÄ±ÅŸÄ± Ã§alÄ±ÅŸabilen bir PWA kabuÄŸuyla birleÅŸtirir. Web push Ã¼Ã§ zorunlu parÃ§adan oluÅŸur: sayfadan baÄŸÄ±msÄ±z Ã§alÄ±ÅŸan bir service worker, push servisine kimliÄŸi kanÄ±tlayan VAPID imzasÄ± ve iÃ§eriÄŸi push servisinin okuyamayacaÄŸÄ± biÃ§imde koruyan uÃ§tan uca ÅŸifreleme. Projedeki WebPush sÄ±nÄ±fÄ± Ã¼Ã§Ã¼nÃ¼ de kendisi yapar: VAPID JWT'sini kurup ES256 ile imzalar ve OpenSSL'in Ã¼rettiÄŸi DER imzayÄ± ham 64 bayta Ã§evirir, geÃ§ici bir ECDH anahtar Ã§ifti Ã¼retip abonenin p256dh anahtarÄ±yla ortak sÄ±r tÃ¼retir, HKDF ile iÃ§erik anahtarÄ± ve nonce Ã¼retir ve iÃ§eriÄŸi aes128gcm (RFC 8188) biÃ§iminde ÅŸifreler. Eski aesgcm biÃ§imi yerine aes128gcm seÃ§ilmiÅŸtir Ã§Ã¼nkÃ¼ tuz ve geÃ§ici anahtarÄ± ayrÄ± baÅŸlÄ±klar yerine gÃ¶vdenin iÃ§inde taÅŸÄ±r. Service worker install, activate, fetch, push, notificationclick ve pushsubscriptionchange olaylarÄ±nÄ± dinler; fetch yalnÄ±zca gezinme isteklerinde devreye girer, bÃ¶ylece bayat bir JavaScript dosyasÄ± Ã¶nbellekte kalmaz. Ã–lÃ¼ abonelikler 404/410 yanÄ±tÄ±nda hemen silinir, geÃ§ici hatalarda yalnÄ±zca sayaÃ§ artar.

- **Gereksinim:** PHP 8.0+ Â· openssl eklentisi (zorunlu) Â· MySQL 5.7+ / MariaDB 10.3+ Â· pdo_mysql Â· mbstring Â· HTTPS (localhost hariÃ§)
- **Kod deposu:** https://github.com/CilginYazilim/pwa-push-notification-system

## Ã–ne Ã‡Ä±kan Ã–zellikler

- 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

## KullanÄ±m SenaryolarÄ±

- 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

## Kurulum

1. Depoyu klonlayÄ±n veya ZIP olarak indirip web kÃ¶kÃ¼nÃ¼ze aÃ§Ä±n
2. database.sql dosyasÄ±nÄ± iÃ§e aktarÄ±n (veritabanÄ±nÄ± kendisi oluÅŸturur)
3. .env.example dosyasÄ±nÄ± .env adÄ±yla kopyalayÄ±n
4. php bin/vapid-keys.php Ã§alÄ±ÅŸtÄ±rÄ±p aÃ§Ä±k anahtarÄ± .env iÃ§ine yapÄ±ÅŸtÄ±rÄ±n
5. HTTPS Ã¼zerinden aÃ§Ä±n (localhost tek istisnadÄ±r) ve bildirimleri aÃ§Ä±n

## AyrÄ±ntÄ±lÄ± AnlatÄ±m

<img src="assets/images/logo.png" alt="Ã‡Ä±lgÄ±n YazÄ±lÄ±m" width="90">

## 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.**

[![PHP](https://img.shields.io/badge/PHP-8.0%2B-777BB4?style=flat-square&logo=php&logoColor=white)](https://php.net)
[![MySQL](https://img.shields.io/badge/MySQL-5.7%2B-4479A1?style=flat-square&logo=mysql&logoColor=white)](https://mysql.com)
[![PWA](https://img.shields.io/badge/PWA-Service_Worker-5a0fc8?style=flat-square)](#service-worker)
[![VAPID](https://img.shields.io/badge/VAPID-ES256-0ea5e9?style=flat-square)](#kritik-kararlar)
[![License](https://img.shields.io/badge/Lisans-MIT-16a34a?style=flat-square)](LICENSE)

**ðŸ‡¹ðŸ‡· TÃ¼rkÃ§e** Â· [ðŸ‡¬ðŸ‡§ English](README.en.md)

[**â–¶ CanlÄ± Demo**](https://cilginyazilim.com/kutuphane/uygulama/pwa-push-notification-system/) Â· [Kaynak KÃ¼tÃ¼phanesi](https://cilginyazilim.com/kutuphane/php-pwa-push-notification) Â· [cilginyazilim.com](https://cilginyazilim.com)

---

### CanlÄ± Demo

**Kurulum yok, kayÄ±t yok, indirme yok â€” tarayÄ±cÄ±nÄ±zdan 3 saniyede deneyin.**

<a href="https://cilginyazilim.com/kutuphane/uygulama/pwa-push-notification-system/"><img src="https://img.shields.io/badge/CANLI_DEMOYU_A%C3%87-0b5cb5?style=for-the-badge&logo=googlechrome&logoColor=white&labelColor=061321" alt="CanlÄ± Demoyu AÃ§" height="42"></a>
<a href="https://cilginyazilim.com/kutuphane/php-pwa-push-notification"><img src="https://img.shields.io/badge/KAYNAK_KODU_%C4%B0NCELE-0ea5e9?style=for-the-badge&logo=readthedocs&logoColor=white&labelColor=061321" alt="Kaynak Kodu Ä°ncele" height="42"></a>
<a href="https://github.com/CilginYazilim/pwa-push-notification-system/archive/refs/heads/main.zip"><img src="https://img.shields.io/badge/ZIP_%C4%B0ND%C4%B0R-16a34a?style=for-the-badge&logo=github&logoColor=white&labelColor=061321" alt="ZIP Ä°ndir" height="42"></a>

<br><br>

<a href="https://cilginyazilim.com/kutuphane/uygulama/pwa-push-notification-system/" title="CanlÄ± demoyu aÃ§mak iÃ§in tÄ±klayÄ±n">
  <img src="docs/screenshots/03-push-bildirimler.png" alt="PWA push bildirim canlÄ± demo Ã¶nizlemesi" width="860">
</a>

<sub>â–² GÃ¶rsele tÄ±klayarak demoyu aÃ§abilirsiniz</sub>

<br>

#### Demo hesaplarÄ±

| Rol | E-posta | Parola |
|---|---|---|
| YÃ¶netici | `admin@cilginyazilim.com` | `Admin1234` |
| KullanÄ±cÄ± | `demo@cilginyazilim.com` | `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 ama `http://` bir sunucuda Ã§alÄ±ÅŸmÄ±yor?** Service worker ve push **gÃ¼venli baÄŸlam** ister. TarayÄ±cÄ±lar `localhost`'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:

1. **Service worker** â€” sayfadan baÄŸÄ±msÄ±z Ã§alÄ±ÅŸan bir betik. Bildirimi gÃ¶steren odur; sekme kapalÄ±yken bile ayaktadÄ±r.
2. **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.
3. **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](https://cilginyazilim.com/kutuphane)** altÄ±nda yayÄ±nlanan aÃ§Ä±klamalÄ±, Ã¼retime hazÄ±r Ã¶rneklerden biridir.

---

### Ä°Ã§indekiler

- [CanlÄ± Demo](#canlÄ±-demo)
- [Bu Proje Nedir?](#bu-proje-nedir)
- [Ekran GÃ¶rÃ¼ntÃ¼leri](#ekran-gÃ¶rÃ¼ntÃ¼leri)
- [Bir bildirimin yolculuÄŸu](#bir-bildirimin-yolculuÄŸu)
- [Kritik Kararlar](#kritik-kararlar)
- [Neler Var?](#neler-var)
- [Service Worker](#service-worker)
- [VAPID anahtarÄ± Ã¼retmek](#vapid-anahtarÄ±-Ã¼retmek)
- [GÃ¼venlik: Neyi, NasÄ±l KapattÄ±k?](#gÃ¼venlik-neyi-nasÄ±l-kapattÄ±k)
- [Kurulum](#kurulum)
- [YapÄ±landÄ±rma](#yapÄ±landÄ±rma)
- [Dosya YapÄ±sÄ±](#dosya-yapÄ±sÄ±)
- [VeritabanÄ± ÅžemasÄ±](#veritabanÄ±-ÅŸemasÄ±)
- [SSS](#sss)
- [CanlÄ± Ortama AlÄ±rken](#canlÄ±-ortama-alÄ±rken)
- [Sorun Giderme](#sorun-giderme)
- [Yol HaritasÄ±](#yol-haritasÄ±)
- [KatkÄ±](#katkÄ±)
- [Lisans](#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.

![Push bildirimler sayfasÄ±: abonelik dÃ¼ÄŸmesi ve kayÄ±tlÄ± cihazlar tablosu](docs/screenshots/03-push-bildirimler.png)

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

![Kontrol paneli: sayaÃ§ ÅŸeridi ve PWA kurulum durumu kartÄ±](docs/screenshots/02-kontrol-paneli.png)

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

![GiriÅŸ ekranÄ±: demo hesaplarÄ± tek tÄ±kla doldurulur](docs/screenshots/01-giris.png)

#### Koyu tema

Tema tarayÄ±cÄ±da deÄŸil **kullanÄ±cÄ± hesabÄ±nda** saklanÄ±r; baÅŸka bir cihazdan girdiÄŸinizde de aynÄ± gelir.

![Koyu tema gÃ¶rÃ¼nÃ¼mÃ¼](docs/screenshots/04-koyu-tema.png)

#### 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`).

<img src="docs/screenshots/05-mobil.png" alt="390px geniÅŸlikte mobil gÃ¶rÃ¼nÃ¼m" width="360">

---

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

```php
'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

```php
// 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:

```php
// 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

```js
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?

<tr><td valign="top" width="50%">

**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-Encoding` ve `Authorization: vapid` baÅŸ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 Â· pushsubscriptionchange
- `manifest.webmanifest` â€” ana ekrana eklenebilir
- `offline.html` â€” aÄŸ yokken gezinme yanÄ±tÄ±

</td><td valign="top" width="50%">

**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

</td></tr>

---

### 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**.

```bash
php bin/vapid-keys.php
```

Betik 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; `.env` iÃ§indeki `VAPID_PUBLIC_KEY` satÄ±rÄ±na yapÄ±ÅŸtÄ±rÄ±n

```env
VAPID_PUBLIC_KEY=BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQ...
VAPID_SUBJECT=mailto:siz@example.com
```

`VAPID_SUBJECT` bir `mailto:` veya `https:` adresidir; push servisi bir sorun olduÄŸunda kime ulaÅŸacaÄŸÄ±nÄ± buradan Ã¶ÄŸrenir.

> **Windows/XAMPP'ta `openssl.cnf` bulunamÄ±yor hatasÄ± alÄ±rsanÄ±z** betik gerekli seÃ§enekleri `WebPush::keygenOptions()` Ã¼zerinden zaten veriyor; yine de sorun sÃ¼rerse `php.ini` iÃ§indeki `openssl.cafile` ve `OPENSSL_CONF` ortam 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

```bash
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Ä±n
```

AÃ§Ä±n: `http://localhost/pwa-push-notification-system/` Â· GiriÅŸ: `admin@cilginyazilim.com` / `Admin1234`

Sonra **Push Bildirimler** sayfasÄ±ndan "Bildirimleri aÃ§" deyin.

---

### YapÄ±landÄ±rma

```env
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:admin@example.com
```

Ã–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

<details>
<summary><b>HTTPS olmadan Ã§alÄ±ÅŸÄ±r mÄ±?</b></summary>

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.
</details>

<details>
<summary><b>iOS'ta Ã§alÄ±ÅŸÄ±yor mu?</b></summary>

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**.
</details>

<details>
<summary><b>Bildirim gitmiyor, hata da yok</b></summary>

SÄ±rayla kontrol edin:

1. **HTTPS** mi? (`localhost` hariÃ§)
2. 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.
3. `.env` iÃ§inde `VAPID_PUBLIC_KEY` dolu mu? Kontrol panelindeki kart size sÃ¶yler.
4. `storage/vapid.pem` dosyasÄ± var mÄ± ve PHP onu okuyabiliyor mu?
5. Ä°ÅŸletim sistemi dÃ¼zeyinde bildirimler kapalÄ± olabilir (Windows'ta "Odak yardÄ±mÄ±", macOS'ta "RahatsÄ±z etmeyin").
</details>

<details>
<summary><b>Toplu bildirim nasÄ±l gÃ¶nderirim?</b></summary>

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](https://cilginyazilim.com/kutuphane/php-queue-worker)'ne bakÄ±n.
</details>

<details>
<summary><b>Neden `web-push-php` kullanmadÄ±nÄ±z?</b></summary>

Ãœ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.
</details>

<details>
<summary><b>Service worker'Ä± gÃ¼ncelledim ama eski sÃ¼rÃ¼m Ã§alÄ±ÅŸÄ±yor</b></summary>

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.
</details>

---

### CanlÄ± Ortama AlÄ±rken

- [ ] **HTTPS zorunlu olsun** â€” push ve service worker bunsuz Ã§alÄ±ÅŸmaz
- [ ] `.env` iÃ§inde `APP_DEBUG=false` (veya satÄ±rÄ± tÃ¼mÃ¼yle silin)
- [ ] `VAPID_PUBLIC_KEY` dolu, `storage/vapid.pem` yerinde
- [ ] `storage/vapid.pem` dosyasÄ±nÄ± **yedekleyin** â€” kaybederseniz tÃ¼m abonelikler geÃ§ersiz olur
- [ ] `storage/` ve `bin/` klasÃ¶rlerinin web'den eriÅŸilemediÄŸini doÄŸrulayÄ±n (403)
- [ ] `service-worker.js` proje **kÃ¶kÃ¼nde** mi?
- [ ] `manifest.webmanifest` iÃ§indeki `start_url` ve simgeler doÄŸru mu?
- [ ] `VAPID_SUBJECT` gerÃ§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
- [ ] `Urgency` baÅŸlÄ±ÄŸÄ± desteÄŸi ve panelden seÃ§ilebilmesi

---

### KatkÄ±

Hata bildirimi ve Ã¶neriler iÃ§in [issue aÃ§abilirsiniz](https://github.com/CilginYazilim/pwa-push-notification-system/issues).

### Lisans

[MIT](LICENSE) â€” ticari projelerinizde de Ã¶zgÃ¼rce kullanabilirsiniz.

---

**[Ã‡Ä±lgÄ±n YazÄ±lÄ±m](https://cilginyazilim.com)** Â· [Kaynak KÃ¼tÃ¼phanesi](https://cilginyazilim.com/kutuphane) Â· [TÃ¼m Ã–rnekler](https://github.com/CilginYazilim)

## SÄ±k Sorulan Sorular

### 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 ve araya giren biri onu enjekte edebilseydi tam kontrolÃ¼ ele geÃ§irirdi. GeliÅŸtirme iÃ§in localhost yeterlidir.

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

### Neden web-push-php kÃ¼tÃ¼phanesi kullanÄ±lmadÄ±?

Ãœ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.

### Toplu bildirim nasÄ±l gÃ¶nderirim?

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.

### VAPID anahtarÄ±nÄ± deÄŸiÅŸtirebilir miyim?

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.

### Bildirim gitmiyor ama hata da yok, ne kontrol etmeliyim?

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.

---

Kaynak: [PWA ve Web Push Bildirim Sistemi](https://cilginyazilim.com/kutuphane/php-pwa-push-notification)
