Implementação de notificações Web Push com PHP 8.0+: guia prático completo
Para acompanhar este artigo
Nível: Avançado
Como implementar notificações Web Push com PHP 8.0+: guia prático completo
O que vamos desenvolver
Neste tutorial, vamos construir um sistema de notificações Web Push utilizando PHP 8.0 ou superior, MySQL, JavaScript e Service Worker.
O objetivo é permitir que uma aplicação envie notificações para os navegadores dos usuários, mesmo quando eles não estiverem com o site aberto.
Ao finalizar, teremos um sistema capaz de:
- Solicitar autorização para enviar notificações.
- Registrar os navegadores dos usuários.
- Armazenar inscrições no MySQL.
- Enviar notificações utilizando VAPID.
- Exibir notificações no navegador.
- Abrir páginas quando o usuário clicar em uma notificação.
- Cancelar inscrições.
- Identificar inscrições expiradas.
- Organizar os envios em uma fila para processamento assíncrono.
Vamos construir primeiro uma versão funcional e depois entender como prepará-la para ambientes de produção.
Como funciona o Web Push?
O Web Push permite que um servidor envie mensagens para um navegador por meio de um serviço intermediário chamado Push Service.
O PHP não precisa manter uma conexão aberta com cada navegador.
O processo funciona assim:
- O usuário acessa o site.
- O navegador solicita autorização para enviar notificações.
- O navegador cria uma inscrição.
- O PHP recebe os dados dessa inscrição.
- O servidor armazena os dados no banco.
- Quando precisamos enviar uma notificação, o PHP utiliza os dados armazenados.
- O serviço Push encaminha a mensagem.
- O Service Worker recebe a mensagem e apresenta a notificação.
Existem três componentes principais:
| ComponenteResponsabilidade | |
| PHP | Registrar inscrições e enviar notificações |
| Push Service | Encaminhar as mensagens |
| Service Worker | Receber e exibir notificações |
Uma observação importante: o serviço Push aceitar uma mensagem não significa que o usuário necessariamente a recebeu ou visualizou.
Requisitos
Para acompanhar o tutorial, precisamos de:
| ComponenteRequisito | |
| PHP | 8.0 ou superior |
| MySQL | 8.0 ou superior |
| Composer | Instalado |
| JavaScript | Navegador com suporte à Push API |
| HTTPS | Obrigatório em produção |
| Extensões PHP | cURL, OpenSSL, JSON e mbstring |
A extensão GMP também é recomendada para melhorar o desempenho de operações criptográficas.
Atenção à compatibilidade: a biblioteca Minishlink possui versões diferentes para cada faixa de PHP:
| PHPVersão da biblioteca | |
| PHP 8.0 | Minishlink 8.x |
| PHP 8.1 | Minishlink 9.x |
| PHP 8.2 ou superior | Minishlink 10.x ou 11.x |
A versão 8.x permite executar o exemplo em PHP 8.0, mas é uma versão antiga, sem manutenção atual. Para projetos novos em produção, prefira PHP 8.2 ou superior e uma versão mantida da biblioteca.
Referência: Minishlink Web Push.
Preparando o projeto
Vamos criar um diretório para organizar nossa implementação.
mkdir webpush-php
cd webpush-phpInicialize o Composer:
composer init --no-interactionInstale a biblioteca compatível com a versão do PHP utilizada.
Para PHP 8.0:
composer require minishlink/web-push:^8.0Para PHP 8.1:
composer require minishlink/web-push:^9.0Para PHP 8.2 ou superior:
composer require minishlink/web-push:^11.0Confira a instalação:
composer show minishlink/web-pushNão misture as três versões no mesmo projeto. O Composer deve resolver e registrar uma versão compatível com o ambiente escolhido.
Guarde o arquivo composer.lock no controle de versão. Ele registra as dependências efetivamente instaladas.
Organizando os arquivos
Vamos utilizar uma estrutura simples:
webpush-php/
├── config/
│ └── config.php
├── public/
│ ├── index.php
│ ├── subscribe.php
│ ├── unsubscribe.php
│ └── service-worker.js
├── src/
│ ├── Database.php
│ └── PushSender.php
├── bin/
│ └── send.php
├── vendor/
├── .env
├── composer.json
└── composer.lockO diretório public será o único acessível pela internet.
Os arquivos de configuração, credenciais e dependências devem permanecer fora da área pública.
Gerando as chaves VAPID
Antes de enviar notificações, precisamos gerar as chaves VAPID.
VAPID é um mecanismo que identifica a aplicação responsável pelo envio das notificações.
Execute:
php -r "require 'vendor/autoload.php'; print_r(Minishlink\WebPush\VAPID::createVapidKeys());"O comando retornará duas chaves:
publicKey: chave pública, utilizada pelo navegador.privateKey: chave privada, utilizada pelo PHP.
Crie um arquivo de configuração com as variáveis de ambiente:
VAPID_PUBLIC_KEY="SUA_CHAVE_PUBLICA"
VAPID_PRIVATE_KEY="SUA_CHAVE_PRIVADA"
VAPID_SUBJECT="mailto:admin@seudominio.com"
DB_HOST="127.0.0.1"
DB_NAME="webpush"
DB_USER="webpush_user"
DB_PASS="SENHA_FORTE"Essas variáveis devem ser configuradas no ambiente do servidor. O PHP não carrega arquivos .env automaticamente.
Nunca coloque a chave privada no JavaScript ou em um diretório público.
Também não gere novas chaves a cada execução. A aplicação deve reutilizar o mesmo par de chaves VAPID.
Criando o banco de dados
Precisamos armazenar as inscrições dos navegadores.
Execute no MySQL:
CREATE DATABASE webpush
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE webpush;
CREATE TABLE push_subscriptions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
endpoint_hash CHAR(64) NOT NULL UNIQUE,
endpoint TEXT NOT NULL,
p256dh VARCHAR(255) NOT NULL,
auth VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
expired_at DATETIME NULL,
INDEX idx_user (user_id),
INDEX idx_expired (expired_at)
) ENGINE=InnoDB;Entenda as colunas:
| ColunaFinalidade | |
id | Identificador da inscrição |
user_id | Usuário proprietário |
endpoint | Endereço do serviço Push |
endpoint_hash | Identificação única do endpoint |
p256dh | Chave pública da inscrição |
auth | Segredo de autenticação |
expired_at | Data de cancelamento ou expiração |
O endpoint é fornecido pelo navegador e não deve ser tratado como uma URL confiável.
Configurando a conexão com o MySQL
Arquivo src/Database.php:
<?php
declare(strict_types=1);
final class Database
{
public static function connection(): PDO
{
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=utf8mb4',
getenv('DB_HOST'),
getenv('DB_NAME')
);
return new PDO(
$dsn,
getenv('DB_USER'),
getenv('DB_PASS'),
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
}
}Essa classe utiliza PDO e consultas preparadas para evitar problemas comuns de segurança nas operações com o banco.
O carregamento das variáveis de ambiente deve acontecer antes da conexão.
Criando a página de inscrição
Agora vamos implementar a parte que o usuário verá.
Arquivo public/index.php:
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notificações Web Push</title>
</head>
<body>
<h1>Notificações Web Push</h1>
<p>Receba novidades diretamente no navegador.</p>
<button id="enable-push">
Ativar notificações
</button>
<button id="disable-push">
Desativar notificações
</button>
<script>
const publicKey = 'SUA_CHAVE_PUBLICA_VAPID';
const csrfToken = <?= json_encode(
$_SESSION['csrf_token']
) ?>;
function decodeKey(value) {
const padding = '='.repeat(
(4 - value.length % 4) % 4
);
const decoded = atob(
(value + padding)
.replace(/-/g, '+')
.replace(/_/g, '/')
);
return Uint8Array.from(
decoded,
character => character.charCodeAt(0)
);
}
document
.getElementById('enable-push')
.addEventListener('click', async () => {
try {
if (
!('serviceWorker' in navigator) ||
!('PushManager' in window)
) {
throw new Error(
'Seu navegador não suporta notificações.'
);
}
const permission =
await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error(
'Permissão não concedida.'
);
}
const registration =
await navigator.serviceWorker.register(
'/service-worker.js'
);
const subscription =
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: decodeKey(publicKey)
});
const response = await fetch(
'/subscribe.php',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(subscription)
}
);
if (!response.ok) {
throw new Error(
'Não foi possível salvar a inscrição.'
);
}
alert('Notificações ativadas!');
} catch (error) {
console.error(error);
alert(error.message);
}
});
document
.getElementById('disable-push')
.addEventListener('click', async () => {
try {
const registration =
await navigator.serviceWorker.ready;
const subscription =
await registration.pushManager.getSubscription();
if (!subscription) {
return;
}
const response = await fetch(
'/unsubscribe.php',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
endpoint: subscription.endpoint
})
}
);
if (!response.ok) {
throw new Error(
'Falha ao cancelar no servidor.'
);
}
await subscription.unsubscribe();
alert('Notificações desativadas.');
} catch (error) {
console.error(error);
alert(error.message);
}
});
</script>
</body>
</html>O que acontece aqui?
- O usuário clica em ativar.
- O navegador solicita autorização.
- O Service Worker é registrado.
- O navegador cria uma inscrição.
- O PHP recebe os dados para armazená-los.
A chave pública VAPID precisa ser substituída pela chave gerada anteriormente.
O exemplo pressupõe uma aplicação com sessão autenticada e usuário identificado no servidor. Em uma aplicação real, o ID do usuário deve vir da sessão, nunca de um valor enviado pelo navegador.
Criando o endpoint para salvar a inscrição
O navegador envia um JSON parecido com este:
{
"endpoint": "https://push.example.com/...",
"expirationTime": null,
"keys": {
"p256dh": "CHAVE_PUBLICA",
"auth": "SEGREDO"
}
}Observe que p256dh e auth estão dentro do objeto keys.
Arquivo public/subscribe.php:
<?php
declare(strict_types=1);
session_start();
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/src/Database.php';
header('Content-Type: application/json; charset=utf-8');
function respond(int $status, array $data): void
{
http_response_code($status);
echo json_encode($data);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
respond(405, ['error' => 'Método não permitido.']);
}
if (empty($_SESSION['user_id'])) {
respond(401, ['error' => 'Autenticação necessária.']);
}
$csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (
empty($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $csrf)
) {
respond(403, ['error' => 'Token CSRF inválido.']);
}
if (
(int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 10000
) {
respond(413, ['error' => 'Requisição muito grande.']);
}
$data = json_decode(
file_get_contents('php://input'),
true
);
if (!is_array($data)) {
respond(400, ['error' => 'JSON inválido.']);
}
$endpoint = $data['endpoint'] ?? '';
$keys = $data['keys'] ?? [];
$p256dh = $keys['p256dh'] ?? '';
$auth = $keys['auth'] ?? '';
if (
!is_string($endpoint) ||
!is_string($p256dh) ||
!is_string($auth) ||
strlen($endpoint) > 4096 ||
strlen($p256dh) > 255 ||
strlen($auth) > 255
) {
respond(400, ['error' => 'Dados inválidos.']);
}
$url = parse_url($endpoint);
if (
!is_array($url) ||
strtolower($url['scheme'] ?? '') !== 'https' ||
empty($url['host']) ||
isset($url['user']) ||
isset($url['pass']) ||
isset($url['port']) && $url['port'] !== 443 ||
!preg_match('/^[A-Za-z0-9_-]+$/', $p256dh) ||
!preg_match('/^[A-Za-z0-9_-]+$/', $auth)
) {
respond(400, ['error' => 'Inscrição inválida.']);
}
try {
$pdo = Database::connection();
$hash = hash('sha256', $endpoint);
$sql = "
INSERT INTO push_subscriptions
(user_id, endpoint_hash, endpoint, p256dh, auth)
VALUES
(:user_id, :hash, :endpoint, :p256dh, :auth)
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
endpoint = VALUES(endpoint),
p256dh = VALUES(p256dh),
auth = VALUES(auth),
expired_at = NULL,
updated_at = UTC_TIMESTAMP()
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'user_id' => $_SESSION['user_id'],
'hash' => $hash,
'endpoint' => $endpoint,
'p256dh' => $p256dh,
'auth' => $auth
]);
respond(200, ['success' => true]);
} catch (Throwable $e) {
error_log($e->getMessage());
respond(500, ['error' => 'Erro ao salvar inscrição.']);
}Esse exemplo precisa ser complementado com uma política de propriedade da inscrição: não permita que um endpoint já associado a outra conta seja transferido automaticamente apenas porque alguém enviou o mesmo endereço.
Também é necessário aplicar uma allowlist de provedores Push e controles de saída de rede. A validação de HTTPS, sozinha, não protege contra SSRF.
Criando o Service Worker
Agora precisamos implementar o arquivo que receberá as notificações.
Arquivo public/service-worker.js:
self.addEventListener('push', event => {
let payload = {};
try {
payload = event.data
? event.data.json()
: {};
} catch {
payload = {};
}
const title = payload.title || 'Saber Dev';
const options = {
body: payload.body || 'Você recebeu uma notificação.',
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: {
url: payload.url || '/'
}
};
event.waitUntil(
self.registration.showNotification(
title,
options
)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
const url = new URL(
event.notification.data.url || '/',
self.location.origin
);
if (url.origin !== self.location.origin) {
return;
}
event.waitUntil(
clients.openWindow(url.href)
);
});O evento push é executado quando uma mensagem chega.
O evento notificationclick é executado quando o usuário clica na notificação.
A validação da origem impede que o clique abra uma página externa inesperada.
Enviando a primeira notificação
Agora chegamos à parte principal: enviar uma mensagem utilizando PHP.
Arquivo src/PushSender.php:
<?php
declare(strict_types=1);
use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;
final class PushSender
{
public static function send(
array $subscription,
array $payload
): bool {
$auth = [
'VAPID' => [
'subject' => getenv('VAPID_SUBJECT'),
'publicKey' => getenv('VAPID_PUBLIC_KEY'),
'privateKey' => getenv('VAPID_PRIVATE_KEY')
]
];
$webPush = new WebPush($auth);
$target = Subscription::create([
'endpoint' => $subscription['endpoint'],
'publicKey' => $subscription['p256dh'],
'authToken' => $subscription['auth']
]);
$report = $webPush->sendOneNotification(
$target,
json_encode(
$payload,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
),
['TTL' => 60]
);
return $report->isSuccess();
}
}A biblioteca cuida da criptografia e da comunicação com o serviço Push.
Observe que não precisamos implementar manualmente o protocolo de criptografia.
Criando um comando para enviar notificações
Não devemos disponibilizar um endpoint público que permita qualquer pessoa enviar notificações.
Para nosso primeiro teste, vamos utilizar um comando executado pelo terminal.
Arquivo bin/send.php:
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/src/Database.php';
require dirname(__DIR__) . '/src/PushSender.php';
if (PHP_SAPI !== 'cli') {
exit("Execute este arquivo pelo terminal.\n");
}
$subscriptionId = (int) ($argv[1] ?? 0);
if ($subscriptionId <= 0) {
exit("Informe o ID da inscrição.\n");
}
$pdo = Database::connection();
$stmt = $pdo->prepare(
"SELECT *
FROM push_subscriptions
WHERE id = :id
AND expired_at IS NULL"
);
$stmt->execute(['id' => $subscriptionId]);
$subscription = $stmt->fetch();
if (!$subscription) {
exit("Inscrição não encontrada.\n");
}
$payload = [
'title' => 'Nova notificação!',
'body' => 'Seu sistema Web Push está funcionando.',
'url' => '/'
];
try {
$success = PushSender::send(
$subscription,
$payload
);
if ($success) {
echo "O serviço Push aceitou a notificação.\n";
} else {
echo "O serviço Push recusou a notificação.\n";
}
} catch (Throwable $e) {
error_log($e->getMessage());
echo "Falha durante o envio.\n";
}Execute:
php bin/send.php 1O número 1 representa o ID da inscrição armazenada no banco.
Se tudo estiver configurado corretamente, o navegador poderá receber a notificação.
Esse exemplo é uma implementação inicial. Em produção, precisamos analisar os relatórios HTTP completos, tratar inscrições expiradas e classificar corretamente as falhas.
Cancelando uma inscrição no servidor
O navegador também precisa conseguir cancelar uma inscrição armazenada.
Arquivo public/unsubscribe.php:
<?php
declare(strict_types=1);
session_start();
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/src/Database.php';
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
if (empty($_SESSION['user_id'])) {
http_response_code(401);
exit;
}
$csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (
empty($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $csrf)
) {
http_response_code(403);
exit;
}
$data = json_decode(
file_get_contents('php://input'),
true
);
$endpoint = $data['endpoint'] ?? '';
if (!is_string($endpoint) || $endpoint === '') {
http_response_code(400);
exit;
}
$hash = hash('sha256', $endpoint);
$pdo = Database::connection();
$stmt = $pdo->prepare(
"UPDATE push_subscriptions
SET expired_at = UTC_TIMESTAMP()
WHERE endpoint_hash = :hash
AND user_id = :user_id
AND expired_at IS NULL"
);
$stmt->execute([
'hash' => $hash,
'user_id' => $_SESSION['user_id']
]);
echo json_encode(['success' => true]);O cancelamento local e o cancelamento no servidor precisam ser coordenados.
Se o navegador cancelar e o servidor falhar, a aplicação deve permitir sincronizar novamente o estado.
Evoluindo para uma fila de notificações
A implementação anterior envia uma notificação diretamente pelo terminal.
Isso funciona para testes e pequenos cenários, mas não é a melhor abordagem para uma aplicação que precisa enviar milhares de mensagens.
Imagine que um artigo seja publicado e precise gerar notificações para 5.000 usuários.
Enviar todas essas mensagens durante a requisição de publicação pode deixar o site lento.
A solução é utilizar uma fila.
O funcionamento passa a ser:
- A aplicação cria uma notificação.
- A notificação é registrada no banco.
- Um worker busca as tarefas pendentes.
- O worker realiza os envios.
- O resultado é armazenado.
Uma tabela para controlar as entregas pode ser criada assim:
CREATE TABLE push_deliveries (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subscription_id BIGINT UNSIGNED NOT NULL,
payload JSON NOT NULL,
status ENUM(
'pending',
'processing',
'accepted',
'failed',
'dead'
) NOT NULL DEFAULT 'pending',
attempts INT UNSIGNED NOT NULL DEFAULT 0,
max_attempts INT UNSIGNED NOT NULL DEFAULT 8,
available_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
locked_until DATETIME NULL,
lease_token CHAR(36) NULL,
last_http_status SMALLINT UNSIGNED NULL,
last_error VARCHAR(1000) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
accepted_at DATETIME NULL,
FOREIGN KEY (subscription_id)
REFERENCES push_subscriptions(id),
INDEX idx_queue (status, available_at, id),
INDEX idx_lease (status, locked_until)
) ENGINE=InnoDB;O worker poderá processar essas tarefas sem bloquear a aplicação principal.
Como tratar falhas?
Nem todo envio será bem-sucedido.
Precisamos classificar os resultados:
| ResultadoAção | |
| HTTP 2xx | Marcar como accepted |
| HTTP 404 ou 410 | Expirar inscrição |
| HTTP 429 | Reagendar |
| HTTP 5xx | Reagendar |
| Falha de conexão | Reagendar |
| HTTP 400 | Investigar os dados |
| HTTP 401 ou 403 | Verificar VAPID |
| Tentativas esgotadas | Marcar como dead |
Para as falhas temporárias, utilize backoff exponencial.
Exemplo:
<?php
declare(strict_types=1);
final class RetryPolicy
{
public static function shouldRetry(
?int $status
): bool {
return $status === null
|| $status === 429
|| ($status >= 500 && $status <= 599);
}
public static function delay(int $attempt): int
{
$attempt = max(1, min($attempt, 10));
$base = min(
3600,
15 * (2 ** ($attempt - 1))
);
return $base + random_int(0, 10);
}
}O intervalo aumenta progressivamente, evitando que o servidor repita requisições rapidamente quando um serviço externo estiver indisponível.
Protegendo o sistema contra SSRF
Existe um risco de segurança que merece atenção.
O endpoint Push é recebido do navegador e posteriormente utilizado pelo servidor para realizar uma conexão HTTP.
Se alguém conseguir cadastrar um endereço malicioso, poderá tentar utilizar o servidor para acessar serviços internos.
Essa vulnerabilidade é chamada de SSRF.
Para evitar esse problema:
- Aceite somente HTTPS.
- Utilize uma allowlist de provedores Push.
- Rejeite endereços IP privados e locais.
- Valide registros DNS A e AAAA.
- Bloqueie conexões para redes internas.
- Desative redirecionamentos automáticos.
- Controle as conexões de saída do servidor.
Uma simples consulta DNS não é suficiente para impedir DNS rebinding. O endereço utilizado na conexão precisa ser o mesmo endereço validado, preservando a validação TLS.
Para produção, implemente essa proteção na camada HTTP e na infraestrutura. A biblioteca deve utilizar um cliente HTTP que respeite essa política.
Referência: OWASP SSRF Prevention Cheat Sheet.
Testando a implementação
Antes de disponibilizar o sistema, verifique cada etapa.
No navegador:
- O botão solicita autorização?
- O Service Worker é registrado?
- A inscrição é criada?
- O navegador recebe a notificação?
- O clique abre a página correta?
- O cancelamento funciona?
No PHP:
- As inscrições são armazenadas?
- As chaves VAPID são carregadas?
- O envio retorna o resultado esperado?
- As falhas são registradas?
- Inscrições expiradas são identificadas?
No MySQL:
- Não existem inscrições duplicadas?
- As inscrições pertencem aos usuários corretos?
- As entregas são armazenadas?
- Os estados são atualizados corretamente?
Para testar em ambiente local, o servidor pode ser iniciado com:
php -S localhost:8000 -t publicO navegador precisa estar em um contexto seguro. localhost é considerado contexto seguro para desenvolvimento. Em produção, utilize HTTPS.
Preparando para produção
Antes de disponibilizar o sistema, implemente:
- Processamento assíncrono com workers.
- Reserva temporária de tarefas.
- Recuperação de processos interrompidos.
- Retentativas automáticas.
- Monitoramento da fila.
- Logs estruturados.
- Limites de conexões simultâneas.
- Proteção contra SSRF.
- Controle de autenticação e CSRF.
- Política de retenção das inscrições.
Para concorrência, o MySQL 8 oferece FOR UPDATE SKIP LOCKED, permitindo que vários workers reservem tarefas diferentes.
O worker deve utilizar tokens de reserva e validar sua propriedade antes de atualizar uma entrega.
Mesmo com esses controles, não existe garantia de entrega exatamente uma vez. Uma falha após o serviço Push aceitar a mensagem, mas antes de o banco registrar o resultado, pode provocar duplicidade em uma nova tentativa.
Conclusão
O Web Push permite criar um canal de comunicação entre uma aplicação e seus usuários sem depender de uma conexão permanente com cada navegador.
Com PHP, MySQL, JavaScript e Service Worker, conseguimos registrar inscrições, enviar mensagens e apresentar notificações diretamente no navegador.
A melhor maneira de desenvolver esse recurso é começar pela inscrição e pelo primeiro envio, validar o funcionamento e depois adicionar recursos como filas, workers, retentativas e monitoramento.
Essa evolução evita complexidade desnecessária no início e permite construir uma arquitetura preparada para crescer.
Referências
- Minishlink Web Push PHP
- Minishlink Web Push no Packagist
- Exemplo oficial de implementação PHP
- MDN: Push API
- MDN: PushSubscription
- MDN: Service Worker API
- MySQL: InnoDB Locking Reads
- OWASP: SSRF Prevention
Neste artigo
Retome suas leituras no histórico. Suas últimas visitas ficam salvas neste navegador, mesmo sem conta.
Este artigo foi útil?
Ajude-nos a melhorar avaliando este conteúdo.
Teste seus conhecimentos
Esta área é exclusiva para usuários cadastrados. Entre ou crie sua conta para responder ao teste.
Comentários
Ainda não há comentários. Seja o primeiro a participar!
Quer participar da conversa? Entre na sua conta para comentar.