Tópicos FCM por plataforma

TLDR: Tópicos FCM por plataforma (platform_ios / platform_android) permitem envio segmentado por SO com uma única chamada ao Firebase, sem fanout manual sobre milhares de tokens.

Visão geral

  • Cada PushDevices registra em topics (ArrayField) a lista de tópicos FCM em que está inscrito
  • Para envio segmentado por SO, o backend usa dois tópicos canônicos: platform_ios e platform_android
  • Os demais tópicos (all / all_staging, refresh_home, user_<id>) continuam sendo inscritos pelo app no momento do registro do device. O backend apenas espelha esses valores no campo topics

Campo topics

Campo Tipo Descrição
topics ArrayField(CharField(100)) Lista de tópicos FCM em que o device está inscrito. Default []

Contratos

Service: apps/notifications/services/topic_subscription.py

Service puro — uma chamada equivale a uma operação sobre um único device. Sem batch, sem responsabilidades de infraestrutura. Pode ser reutilizado em qualquer fluxo (signal de registro de device, jornadas futuras, etc.).

Função Descrição
subscribe(device, topics) Inscreve o device em cada tópico via FCM. Persiste em device.topics apenas quando o FCM aceita. Retorna False se o token está morto. Idempotente
unsubscribe(device, topics) Análogo, removendo do array

Tokens mortos e exceções (FirebaseError, ValueError) são apenas logados — não são removidos do banco nesta fase.

Envio segmentado por plataforma

```python from firebase_admin import messaging

msg = messaging.Message( topic=”platform_ios”, # ou platform_android notification=messaging.Notification(title=”…”, body=”…”), ) messaging.send(msg) ```

Para envio cruzado:

python msg = messaging.Message( condition="'platform_ios' in topics || 'platform_android' in topics", notification=..., )

Enviar pelo Admin

O campo Notification.platform_target (choices ios/android, opcional) permite ao admin enviar direto para o tópico da plataforma. Precedência do roteamento em NotificationService._get_target:

platform_target > send_to_all > push_device

platform_target e send_to_all são mutuamente exclusivos — o form do admin recusa o save quando os dois vêm preenchidos.

Backfill via data migration

A migration 0017_backfill_platform_topics.py roda automaticamente no python manage.py migrate e:

  1. Itera todos os PushDevices com token e platform em (ios, android)
  2. Chama subscribe(device, [platform_<os>]) no FCM — única chamada externa
  3. Acrescenta ao campo topics os tópicos legados que o mobile já gerencia: all_<env> (all_staging em staging, all em prod), refresh_home, user_<id>
  4. Falhas FCM são ignoradas por device (apenas log)

Não é necessário rodar nada no shell — basta o migrate normal do deploy.

Inscrição automática no registro do device

O signal post_save de PushDevices (com created=True) dispara a task subscribe_device_to_default_topics, que inscreve o device em all/all_staging, refresh_home e platform_<os>. O subscribe é idempotente, então a inscrição feita pelo app no login permanece como redundância segura.

Expiração e unsubscribe

O envio para tópico é feito pelo Firebase e ignora a flag is_expired do banco. Por isso, todo ponto que marca um device como expirado também chama unsubscribe(device, device.topics) — sem isso, tokens antigos ainda entregáveis continuam recebendo o canal “All”, gerando notificações duplicadas.

Script de teste manual

scripts/test_platform_topic_subscription.py — ajustar USER_EMAIL e TARGET_PLATFORM no topo e rodar:

bash python manage.py shell < scripts/test_platform_topic_subscription.py

Inscreve os devices do usuário no tópico e envia uma mensagem para confirmar a entrega.

Referências