@3apps/fcm-notifications
v1.0.0
Published
Notificações push automáticas por segmento (país ou idioma) com Firebase: Remote Config → Cloud Scheduler → Cloud Tasks → FCM, no fuso de cada segmento.
Maintainers
Readme
@3apps/fcm-notifications
Notificações push automáticas com Firebase, por segmento (país ou idioma), no fuso de cada segmento. O conteúdo e os horários ficam no Remote Config; a lib cria as Cloud Functions que agendam e enviam.
Cloud Scheduler (criado pelo deploy, uma vez por dia)
└─▶ fcmScheduleNotification ── lê o Remote Config (template de SERVIDOR)
└─▶ Cloud Tasks: fila fcmSendNotification (uma task por disparo, no horário local do segmento)
└─▶ fcmSendNotification ──▶ FCM, tópico do segmento (ex.: score_good_US)Uso
Na pasta das functions do app (Firebase Functions v2, Node 20+):
npm install @3apps/fcm-notifications firebase-admin firebase-functions// index.js
const { createNotificationWorkflow } = require('@3apps/fcm-notifications');
module.exports = createNotificationWorkflow(require('./notifications.config'));// notifications.config.js
module.exports = {
projectId: 'meu-projeto',
serviceAccount: '[email protected]',
topics: ['all', 'score_poor', 'score_fair', 'score_good', 'score_very_good'],
topicName: (topic, segment) => `${topic}_${segment}`,
android: { channelId: 'high_importance_channel', icon: 'ic_stat_notification', color: '#009E83' },
};O index.js exporta as duas funções com os nomes de functionNames. Se o app tiver outras
funções no mesmo codebase, espalhe o objeto: Object.assign(exports, createNotificationWorkflow(...)).
Configuração
| Campo | Obrigatório | Padrão | Para que serve |
|---|---|---|---|
| projectId | sim | | Projeto Firebase. Usado pela CLI. |
| serviceAccount | sim | | Conta de serviço que executa as funções, assina o token OIDC do job e das tasks e é a única que pode enfileirar e invocar o envio. |
| topics | sim | | Tópicos base que o app assina. Tópicos de fora da lista são ignorados com aviso. |
| topicName | | (topic, segment) => \${topic}${segment}`| Nome do tópico FCM.segment é o código da chave em maiúsculas (US, PT). Precisa ser igual à regra do app. |
| android| | nenhum |channelId, iconecolorda notificação no Android. Sem ícone, o Android usa o ícone do app, que costuma virar um quadrado branco. |
|region| |us-central1| Região das funções e da fila. |
|maxInstances| |10| |
|functionNames| |{ schedule: 'fcmScheduleNotification', send: 'fcmSendNotification' }| O nome da função de envio é também o nome da fila. |
|job| |{ schedule: '0 0 * * *', timeZone: 'America/Sao_Paulo', retryCount: 3, minBackoffSeconds: 300, timeoutSeconds: 540 }| Quando o job roda e como repete em caso de erro. |
|send| |{ maxAttempts: 3, minBackoffSeconds: 60 }| Tentativas de envio por task. |
|lookaheadHours| |25| Janela de cada execução. Precisa ser maior que o intervalo entre execuções do job (ver abaixo). |
|remoteConfig| |{ keyPrefix: 'fcm_notifications', dir: 'remote-config' }| Prefixo das chaves e pasta dos JSONs no app (relativa aonotifications.config.js`). |
Remote Config
Cada segmento tem uma chave do tipo JSON no template de servidor (Remote Config → aba
Servidor), <keyPrefix><código de 2 letras minúsculas>: fcm_notifications_us,
fcm_notifications_pt. O job descobre os segmentos pelas chaves: para incluir um segmento,
basta criar a chave.
{
"enabled": true,
"time_zone": "America/New_York",
"topics": {
"score_good": {
"schedules": [12, "19:30"],
"notifications": [
{
"title": "Good credit opens doors",
"body": "See the cards that match a Good credit profile.",
"data": { "open_feature": "ranking_cards" },
"analytics_label": "opcional"
}
]
}
}
}| Campo | Regra |
|---|---|
| enabled | false pausa o segmento sem apagar o conteúdo. Se omitido, vale true. |
| time_zone | Fuso IANA. Os schedules são horários locais desse fuso, com horário de verão. |
| topics | Tópicos base (de config.topics). O tópico FCM é config.topicName(tópico, segmento). |
| schedules | Hora cheia (12) ou "HH:MM". |
| notifications | title e body obrigatórios. Cada horário de um mesmo dia recebe uma notificação diferente, com unicidade pelo título; horários que sobrarem ficam sem disparo. |
| data | Enviado como data da mensagem FCM (valores viram texto). É onde vai o deep link do app. |
| analytics_label | Opcional, [a-zA-Z0-9-_.~%]{1,50}. Se omitido, é derivado do tópico + título, para os relatórios do FCM separarem segmentos com o mesmo texto. |
Mantenha os JSONs numa pasta do app (remote-config/) e publique com a CLI: assim eles ficam
versionados, validados e testáveis.
Como o agendamento funciona
- Uma execução por dia, janela de 25h. Cada execução enfileira todos os horários das próximas 24h de cada segmento, no fuso dele. Com segmentos em fusos diferentes, "o resto do dia" não serve: num mesmo instante, cada um está numa hora diferente. Uma janela de 24h contém cada horário uma vez, e as execuções seguidas se encaixam, inclusive nos dias de horário de verão. A hora a mais cobre atraso ou retry do job (um horário que caia no instante exato do job já foi agendado pela execução anterior).
- Sem disparo duplicado. O id de cada task é fixo para o disparo: hash + tópico + data local +
horário. Rodar de novo, forçar o job ou o retry do Scheduler não duplicam: a Cloud Tasks recusa
o id repetido (
functions/task-already-exists), e o resumo conta como "id já usado". O formato do id faz parte do contrato da lib: mudar o formato é versão major. - Sem notificação repetida no dia. A ordem das notificações de cada dia é embaralhada com a semente tópico + data, então os horários de um mesmo dia não repetem notificação, mesmo quando são agendados por execuções diferentes.
- Erros. JSON inválido ou task não criada falham a execução (aparece no Cloud Scheduler e o
retry entra), mas os outros segmentos são agendados. Payload inválido no envio é registrado e
descartado sem retry; erro do FCM é repetido pela Cloud Tasks até
send.maxAttempts.
CLI
Na pasta das functions do app, com as credenciais locais
(gcloud auth application-default login):
npx fcm-notifications dry-run # disparos da janela com o Remote Config de produção
npx fcm-notifications dry-run --local --hours=48 # com os JSONs locais e outra janela
npx fcm-notifications publish-remote-config # prévia campo a campo (publicado → local)
npx fcm-notifications publish-remote-config --apply # publica as chaves <keyPrefix>*
npx fcm-notifications send-push --token=<token> # push de teste num device
npx fcm-notifications send-push --topic=score_good_US --validate-only # só valida no FCMpublish-remote-configvalida cada JSON com a regra do job, só mexe nas chaves dos segmentos e preserva o resto do template. Se a última versão foi editada no console, recusa publicar (para não desfazer a edição): leve a mudança para o arquivo ou use--overwrite-console.send-pushaceita--title=,--body=e--data='{"chave":"valor"}'. Tópico real só com--validate-only, para um teste não virar push para todo mundo.- Use
--config=caminhose o arquivo não fornotifications.config.js.
Deploy num projeto novo
PROJECT=meu-projeto
SA=fcm-notifications@$PROJECT.iam.gserviceaccount.com
# 1. Conta de serviço (o deploy falha sem ela)
gcloud iam service-accounts create fcm-notifications --project=$PROJECT
for ROLE in roles/cloudconfig.viewer roles/firebasecloudmessaging.admin; do
gcloud projects add-iam-policy-binding $PROJECT --member=serviceAccount:$SA --role=$ROLE --condition=None
done
gcloud iam service-accounts add-iam-policy-binding $SA \
--member=serviceAccount:$SA --role=roles/iam.serviceAccountUser --project=$PROJECT
# 2. Conteúdo no Remote Config de servidor
npx fcm-notifications publish-remote-config --apply
# 3. Deploy (cria as funções, o job e a fila)
firebase deploy --only functions
# 4. Uma vez, na fila recém-criada (ver "Problemas conhecidos")
gcloud tasks queues add-iam-policy-binding fcmSendNotification --location=us-central1 \
--member=serviceAccount:$SA --role=roles/cloudtasks.enqueuer --project=$PROJECTO papel do Remote Config é roles/cloudconfig.viewer ("Firebase Remote Config Viewer"); não
existe roles/firebaseremoteconfig.viewer.
Problemas conhecidos
- Build sem permissão em projeto novo: o Cloud Build usa a conta padrão do Compute, que vem
sem papéis. Dê
roles/cloudbuild.builds.buildera<número>[email protected]. - Função em estado
FAILEDdepois de um build com erro: os deploys seguintes respondemPrecondition failed. Apague a função e faça o deploy de novo; se ofirebase functions:deletedo job parar no 404 do Scheduler, usegcloud functions delete. - Fila sem enfileirador: o Firebase CLI quebra ao dar o papel de enfileirador numa fila
recém-criada (
Cannot read properties of undefined (reading 'filter'), bug dosetEnqueuerdo firebase-tools com política vazia). As funções ficam ativas, mas o job não enfileira. É o passo 4 acima; nos deploys seguintes não se repete. - Não apague tasks para testar. A Cloud Tasks reserva o id de uma task apagada ou executada
por até 24 horas (a documentação do firebase-admin ainda fala em ~1 hora, desatualizada),
e nem limpar a fila libera. Enquanto isso, o job não consegue recriar esses disparos. Para
testar, use
dry-run. - firebase-admin 14 não instala o
@google-cloud/firestore, e importarfirebase-functions/v2(o índice) carrega o provider do Firestore. A lib importa só os módulos que usa (/v2/scheduler,/v2/tasks); faça o mesmo no app.
Desenvolvimento da lib
npm test # testes, sem rede
npm version minor # ou patch/major; cria a tag no git
npm publishfirebase-admin e firebase-functions são peerDependencies: a lib usa as versões instaladas
no app. Para testar uma mudança num app antes de publicar, instale a pasta como cópia (e não
link), para as dependências virem do app: npm install --install-links ../caminho/fcm-notifications.
