Pular para o conteúdo
🔔 PUSH

Push no iPhone e no Android, disparado de onde o dado muda.

O app registra o aparelho. Você manda pelo seu servidor ou de dentro do banco — um gatilho na tabela de jogadas avisa o próximo jogador. O SuperDB entrega pela Apple e pelo Google e diz o que aconteceu com cada aparelho.

Como funciona

  1. Uma vez, no painel: você cola a chave de push da Apple (.p8) e a conta de serviço do Firebase. O SuperDB confere as duas com a Apple e com o Google antes de guardar.
  2. No app: a cada abertura, o app manda o token do aparelho para POST /push/v1/aparelhos. O aparelho fica ligado a um dono — o id do usuário no seu app.
  3. Para mandar: do seu servidor, POST /push/v1/enviar com a chave de servidor; ou de dentro do banco, push.enviar() numa função do projeto. Você diz os donos; o SuperDB acha os aparelhos de cada um.
  4. O recibo: para cada aparelho, se a Apple ou o Google aceitou — e, quando recusou, por quê. Aparelho de app desinstalado sai da lista sozinho no primeiro envio que falhar.
💡

No iPhone o SuperDB fala direto com a Apple. No Android não há outro caminho: o Google só entrega push pelo Firebase Cloud Messaging. O Firebase aqui é só o cano — os aparelhos, a regra de quem recebe e o recibo ficam no SuperDB.

1. As chaves (uma vez por projeto)

No painel, abra o projeto → Push.

Apple (iPhone)

  1. No Apple Developer: Certificates, IDs & Profiles → Keys → +, marque Apple Push Notifications service (APNs) e baixe o arquivo AuthKey_XXXXXXXXXX.p8. A Apple deixa baixar uma vez só: guarde.
  2. No painel, suba o .p8 e preencha o Key ID (os 10 caracteres do nome do arquivo), o Team ID (em Membership) e o Bundle ID (o ios.bundleIdentifier do app.json).
  3. O painel confere com a Apple nos dois ambientes — produção (App Store, TestFlight) e sandbox (build de desenvolvimento) — e diz se algum recusou.

Android (Firebase Cloud Messaging)

  1. No console do Firebase: Configurações do projeto → Contas de serviço → Gerar nova chave privada. Baixa um JSON com "type": "service_account".
  2. Suba esse JSON no painel. O google-services.json é outro arquivo: ele vai dentro do app (android.googleServicesFile no app.json).
  3. O painel confere com o Google. Se ele disser que a API está desligada, ative a Firebase Cloud Messaging API no Google Cloud do mesmo projeto — o recado do Google traz o link.

A .p8 e a chave privada do Google ficam cifradas com AES-256-GCM, com uma chave que só abre no contexto do seu projeto, e nenhuma resposta da API as devolve. Sem painel, o mesmo pela API de gestão (management key sdb_pmk_):

curl -X PUT https://auth.superdb.com.br/platform/v1/projects/<projectId>/push/credenciais/apns \
  -H "Authorization: Bearer $SUPERDB_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile p8 AuthKey_ABC123DEF4.p8 \
        '{key_id: "ABC123DEF4", team_id: "TEAM123456", bundle_id: "br.com.suaempresa.app", p8: $p8}')"

# Android: PUT .../push/credenciais/fcm  com  {"conta_de_servico": "<o JSON inteiro>"}

2. Registrar o aparelho (no app)

Com Expo, o token nativo vem de getDevicePushTokenAsync(). Não use o ExponentPushToken: ele só funciona pelo serviço de push do Expo, e a API recusa com token_do_expo.

import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native'

export async function registrarAparelho(superdb, anonKey, idDoJogador) {
  const { status } = await Notifications.requestPermissionsAsync()
  if (status !== 'granted') return

  const { data: token } = await Notifications.getDevicePushTokenAsync()
  // Logado: o data_plane_token da sessão — o dono vira o usuário, verificado.
  // Sem login: a anon key + um id seu no campo "dono" (não verificado).
  const sessao = await superdb.auth.getDataPlaneToken()

  await fetch('https://auth.superdb.com.br/push/v1/aparelhos', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${sessao ?? anonKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      token,
      plataforma: Platform.OS === 'ios' ? 'ios' : 'android',
      ...(sessao ? {} : { dono: idDoJogador }),
      versao_app: '1.4.0',
    }),
  })
}
CampoO que é
tokenO data de getDevicePushTokenAsync(). No iPhone é hexadecimal; no Android, o token do FCM.
plataformaios ou android.
ambienteOpcional: producao (padrão) ou sandbox. Se errar, tudo bem: quando a Apple diz que o token não é daquele ambiente, o SuperDB tenta o outro e grava o certo.
donoO id do usuário no seu app, até 200 caracteres. Com a sessão do usuário, é ignorado — o dono é quem está logado.
versao_appOpcional, até 60 caracteres. Aparece no painel.

A resposta é 201 na primeira vez e 200 nas seguintes (o mesmo token atualiza a mesma linha): { "id", "dono", "dono_verificado", "ativo": true }. Guarde o id se quiser mandar para um aparelho específico.

Quem é o dono

Credencial usada no registroDonoVerificado
Sessão do usuário (data_plane_token)O usuário logadoSim
Chave de servidor (service_role)O dono do corpoSim — é o seu backend dizendo
Chave anonO dono do corpoNão
⚠️

A chave anon é pública: qualquer um que a extraia do app pode registrar um aparelho em nome de qualquer id. Sem login, não ponha no push o que não pode vazar — "é sua vez na sala 4" pode; o saldo da conta, não.

Quem tem login fica protegido de três jeitos: a chave anon não pode declarar como dono um usuário do Auth do projeto (403 dono_e_usuario — esse registro é com a sessão); um registro com a anon não troca o dono de um aparelho que uma sessão verificou; e um dono que tem aparelho verificado não recebe nos aparelhos só declarados.

Se o seu app tem identidade própria (sem o Auth do SuperDB), registre pelo seu servidor, com a chave de servidor: o dono vira verificado.

No logout, descadastre — senão o próximo usuário daquele celular recebe o push do anterior:

await fetch('https://auth.superdb.com.br/push/v1/aparelhos', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${anonKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ token }),
})
📱

Teste num development build (EAS), não no Expo Go: no Expo Go o token nativo é do app Expo Go, e a Apple recusa com DeviceTokenNotForTopic. Com o plugin do expo-notifications no app.json, o build do iOS já sai com a capacidade de push ligada.

3. Mandar pelo seu servidor

Só com a chave de servidor (service_role), que nunca vai no app. Com a anon ou com a sessão, a API responde 403 chave_de_servidor.

curl -X POST https://auth.superdb.com.br/push/v1/enviar \
  -H "Authorization: Bearer $SUPERDB_SERVICE_ROLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destinatarios": ["id-do-jogador"],
    "titulo": "É sua vez",
    "corpo": "A Ana jogou. A mesa está esperando você.",
    "dados": { "sala": "sala-4" },
    "opcoes": { "agrupar": "sala-4", "ttl": 600 }
  }'

Resposta 202 { "id", "status": "pendente", "recibo": "/push/v1/envios/<id>" }. O envio entra numa fila e sai em cerca de um segundo.

CampoRegra
destinatariosIds dos donos. Até 500 por envio — divida em lotes.
aparelhosOpcional: ids de aparelho (os do registro), até 500.
titulo / corpoDe 1 a 200 / de 1 a 1.000 caracteres.
dadosObjeto JSON de até 3 KB. Chega no app junto com a notificação (no Android, cada valor vira texto). Título, corpo e dados somados: até 3,5 KB — a Apple aceita 4 KB por notificação.

As opcoes, todas opcionais — uma opção inválida é ignorada, não derruba o envio:

OpçãoEfeito
ttlSegundos que a Apple e o Google seguram a mensagem se o aparelho estiver desligado (até 28 dias).
agruparMensagens com a mesma chave se substituem no aparelho — "é sua vez" da mesma sala não empilha. Até 64 bytes.
somNome do arquivo de som do app, ou null para silencioso. O padrão é o som padrão.
badgeO número no ícone (iPhone).
prioridadealta (padrão) ou normal.
canal_androidO canal de notificação do Android (precisa existir no app).

4. Mandar de dentro do banco

Quando o dado muda no banco, o push pode sair dali mesmo — sem servidor seu no meio. Rode no SQL Editor do painel (pode rodar de novo sem medo):

superdb/push.sql
-- Quem está em cada sala. O seu servidor (ou uma função sua) grava quem entrou.
create table if not exists sala_jogadores (
  sala text not null,
  jogador uuid not null,
  primary key (sala, jogador)
);

-- A jogada: quem jogou e para quem passou a vez.
create table if not exists jogadas (
  id bigint generated always as identity primary key,
  sala text not null,
  jogador uuid not null default auth.uid(),
  proximo_jogador uuid not null,
  feita_em timestamptz not null default now()
);

alter table sala_jogadores enable row level security;
alter table jogadas enable row level security;

-- "Esta pessoa está nesta sala?" SECURITY DEFINER: lê sala_jogadores com a
-- permissão do dono do projeto, sem dar leitura da tabela a ninguém.
create or replace function na_sala(p_sala text, p_jogador uuid) returns boolean
language sql stable security definer set search_path from current as $$
  select exists (select 1 from sala_jogadores where sala = p_sala and jogador = p_jogador)
$$;

-- Só joga quem está na sala, e só passa a vez para quem também está.
drop policy if exists jogar on jogadas;
create policy jogar on jogadas
  for insert to authenticated
  with check (jogador = auth.uid() and na_sala(sala, auth.uid()) and na_sala(sala, proximo_jogador));

revoke all on sala_jogadores, jogadas from anon, authenticated;
grant insert on jogadas to authenticated;

-- SECURITY DEFINER: roda como o dono do projeto, e é esse papel que o
-- push.enviar confere para saber de qual projeto o push sai.
create or replace function avisa_proximo() returns trigger
language plpgsql security definer set search_path = pg_catalog, pg_temp as $$
begin
  -- No máximo um aviso por jogador a cada 30 segundos: jogada repetida não vira
  -- enxurrada no celular de ninguém, nem gasta o limite do mês.
  if exists (select 1 from push.envios
              where destinatarios = array[new.proximo_jogador::text]
                and criado_em > now() - interval '30 seconds') then
    return new;
  end if;
  perform push.enviar(
    array[new.proximo_jogador::text],
    'É sua vez',
    'A mesa está esperando você',
    jsonb_build_object('sala', new.sala),
    jsonb_build_object('agrupar', new.sala)
  );
  return new;
end $$;

drop trigger if exists avisa_proximo on jogadas;
create trigger avisa_proximo after insert on jogadas
  for each row execute function avisa_proximo();

Duas travas da receita valem para qualquer push disparado de dentro do banco: a policy decide para quem se pode mandar (aqui, só para quem está na mesma sala) e o gatilho segura a frequência (um aviso por jogador a cada 30 segundos). Sem as duas, qualquer usuário logado mandaria push para qualquer outro, quantas vezes quisesse — e gastaria o limite do mês do seu projeto.

A assinatura: push.enviar(destinatarios text[], titulo text, corpo text, dados jsonb default '{}', opcoes jsonb default '{}') returns uuid — devolve o id do envio. As regras de tamanho são as mesmas da API. Dentro da função, push.envios mostra só os envios do seu projeto — é o que o freio da receita consulta.

⚠️

push.enviar precisa rodar como o dono do projeto — por isso a função é security definer, criada pelo SQL Editor ou pela API de SQL. Chamado direto pela anon, pela sessão ou pela service_role, ele recusa: esses papéis são os mesmos em todos os projetos e não dizem de qual projeto o push sairia. E se expuser uma função dessas por RPC, confira dentro dela quem pode avisar quem — senão qualquer usuário manda push para qualquer outro.

5. O recibo

Pelo servidor: GET /push/v1/envios/<id> com a chave de servidor. No painel: Push → Últimos envios. No SQL Editor: select * from push.envios e select * from push.entregas — cada projeto enxerga só os próprios.

{
  "id": "…", "status": "concluido", "total": 2, "aceitos": 1, "recusados": 1,
  "entregas": [
    { "dono": "id-do-jogador", "plataforma": "android", "resultado": "aceito", "motivo": null },
    { "dono": "id-do-jogador", "plataforma": "ios", "resultado": "recusado",
      "motivo": "apns:Unregistered",
      "explicacao": "O app foi desinstalado ou as notificações foram desligadas neste iPhone. O aparelho saiu dos ativos." }
  ]
}
MotivoO que fazer
apns:Unregistered, fcm:UNREGISTEREDNada: o app saiu do aparelho, e o aparelho saiu dos ativos.
apns:BadDeviceToken, apns:DeviceTokenNotForTopicO token é de outro app ou de outro Bundle ID (Expo Go, por exemplo).
apns:InvalidProviderTokenA Apple não aceitou a chave: confira Key ID, Team ID e a .p8 no painel.
fcm:SENDER_ID_MISMATCHA conta de serviço é de outro projeto do Firebase que o do google-services.json.
sem_credencial_apns, sem_credencial_fcmFalta a chave daquela plataforma no painel.
cota_mensal_esgotadaAcabou o limite do plano no mês.
apns:indisponivel, fcm:indisponivel, prazo_esgotadoA Apple ou o Google estavam fora do ar ou lentos, e o resto do envio foi interrompido para não segurar a fila. Não conta no limite: mande de novo.
apns:chave_de_outro_ambienteA chave da Apple está restrita a produção ou a sandbox, e o aparelho é do outro ambiente.

"Aceito" quer dizer que a Apple ou o Google recebeu. Se a notificação não aparecer, a causa mais comum é a permissão de notificação desligada no aparelho.

Limites e entrega

  • Por mês: cada aparelho que foi à Apple ou ao Google conta um envio, aceito ou não; recusa nossa (sem credencial, fora do limite, mensagem grande demais, serviço fora do ar) não conta. O limite depende do plano — no Grátis e no Site é para construir e testar; os números estão em preços. Passou do limite, POST /push/v1/enviar responde 429 cota_mensal_esgotada com Retry-After até a virada do mês (meia-noite de Brasília).
  • Fila: até 1.000 envios pendentes por projeto; passou disso, 429 fila_cheia.
  • Entrega pelo menos uma vez: se o servidor cair no meio de um envio, ele volta para a fila e algum aparelho pode receber duas vezes. Com agrupar, a segunda substitui a primeira.
  • Até 10 aparelhos ativos por dono: quando entra o 11º, o que está há mais tempo sem abrir o app sai.

Dados e LGPD

O aparelho liga um celular a uma pessoa, e é tratado como dado pessoal:

  • Quando o usuário apaga a conta, os aparelhos dele param de receber na hora e saem de vez na exclusão definitiva. O GET /auth/v1/lgpd/export traz os aparelhos cadastrados (sem o token).
  • Recibos e envios ficam 30 dias. Aparelho desativado sai depois de 90 dias; aparelho que não abre o app há 270 dias também sai.
  • O SQL do projeto lê os aparelhos sem a coluna do token, e ninguém além da plataforma lê as credenciais.

O que ainda não fazemos

  • Push na web (notificação no navegador) — ainda não.
  • Token do Expo (ExponentPushToken) — não: o caminho é o token nativo.
  • Agendar um envio para depois, e tópicos ou segmentos — ainda não. Para agendar, dispare do seu servidor na hora certa.
  • Método no SDK — ainda não: por enquanto é fetch, como nos exemplos.