30

I'm trying to find a way to send a notification using Firebase Cloud Messaging to all of my app's users, but I have a web-only app. I've seen solutions that appear to be for Android/iOS, which basically involves having the users auto-subscribe to a topic called "allDevices" and then sending a notification to all users subscribed to that topic. I just can't seem to find any documentation on how to have a web-based user subscribe to a topic. Does anyone know if that's possible, and if it is, is there documentation that I've missed that would cover that?

Thanks!

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Derek
  • 493
  • 2
  • 5
  • 9

5 Answers5

34

There is no direct API to subscribe clients to topics in the Firebase Cloud Messaging SDK for JavaScript. Instead you can subscribe a token to a topic through the REST API. Calling this API requires that you specify the FCM server key, which means that you should only do this on a trusted environment, such as your development machine, a server you control, or Cloud Functions. This is necessary, since having the FCM server key allows sending messages on your app's behalf to all of your app's users.

It turns out that in my tests I was using an older project, where client API keys were allows to subscribe to topics. This ability has been removed from newer projects for security reasons.

In Node.js you could for example call the REST API to create a relation mapping for an app instance like this:

function subscribeTokenToTopic(token, topic) {
  fetch('https://iid.googleapis.com/iid/v1/'+token+'/rel/topics/'+topic, {
    method: 'POST',
    headers: new Headers({
      'Authorization': 'key='+fcm_server_key
    })
  }).then(response => {
    if (response.status < 200 || response.status >= 400) {
      throw 'Error subscribing to topic: '+response.status + ' - ' + response.text();
    }
    console.log('Subscribed to "'+topic+'"');
  }).catch(error => {
    console.error(error);
  })
}

Where fcm_server_key is the FCM server key, taken from the Firebase console of your project.


Update: the option to subscribe a FCM token a topic is now also part of most of the Firebase Admin SDK, which makes it easier to do this on supported environments. For more on this, see the documentation on subscribing the client app to a topic/

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thanks, Frank! This looks like the alley I need to go down. Is there something I need to setup with my Firebase project in order to use the IID API? Do I need to predefine a topic somewhere/somehow? I attempted to use this function, but I'm getting a 401 error in the console: "Error subscribing to topic: 401 - [object Promise]". For reference, I have verified that the token is correct (I can send a targeted notification to the specific token) and my apiKey is set properly as well. – Derek Nov 03 '16 at 13:40
  • Oh, and for further context, I'm just learning Firebase. I've gotten the Firebase team's JS messaging quickstart project up and successfully running, so if it helps to know what the code base looks like, this is what I'm using: https://github.com/firebase/quickstart-js/tree/master/messaging. – Derek Nov 03 '16 at 13:48
  • Topics are automatically created when a device subscribes to them or when you send a message to them (using the API). – Frank van Puffelen Nov 03 '16 at 13:52
  • Okay, cool. That's kind of what I expected. Unfortunately that means there's some other reason why I'm having problems... if I'm understanding correctly the IID reference that you linked to, the 401 error occurs due to an invalid header. I tried adding Content-Type to the header in your code like this: `headers: new Headers({ 'Content-Type' : 'application/json', 'Authorization': 'key='+config.apiKey })` Unfortunately, I'm still getting a 401 error. I am perhaps misunderstanding what a 401 error means here? – Derek Nov 03 '16 at 14:14
  • I know this function works, since I use it in multiple projects at the moment. If it doesn't work for you, that is likely something specific to your code/project/setup. Stack Overflow is a notoriously inefficient mechanism to interactively debug your app. If you're having problems getting specific code to work, share the [minimal code that reproduces the problem](http://stackoverflow.com/help/mcve) in your question. – Frank van Puffelen Nov 03 '16 at 15:17
  • Thanks for your responses, Frank, I appreciate it. For some reason, it boiled down to my Firebase project's API key. I'm not sure why at this point, but I had a different API key on my project yesterday than what I see today when I look at my project in the Firebase console. The code that gets auto-generated to add Firebase to my web app now shows a different API key than what I wrote down yesterday. So basically, the app works with that generated code, but in order to get your function to work, I had to hard code my different key from yesterday. I have no idea why or how it changed... weird. – Derek Nov 03 '16 at 20:34
  • That sounds weird indeed. Not sure what is going on there. Just make sure that it is a client API key and not something that is not supposed to only be present on the server. – Frank van Puffelen Nov 03 '16 at 23:11
  • Aha! I figured out the key confusion! It turns out I had written down the Cloud Messaging Server Key (found on the Firebase console under Project Settings > Cloud Messaging), which I had to hard code as the API Key in the function you provided, @frank-van-puffelen. This is different from the Web API Key (in Project Settings > General), which is the config.apiKey as generated by Firebase when I click on "Add Firebase to your web app" in the Firebase console. Would you happen to know the difference between these two keys? – Derek Nov 04 '16 at 13:07
  • 2
    You should *never* expose the server key in client-side code. In my tests the code also works with the client-side API key. But I heard rumors that this was unintentional, so it could be that the behavior changed. Interestingly enough though: I just tested and I am still able to subscribe with *my* client-side API key, so it may be that this behavior was changed recently or only for newer keys. – Frank van Puffelen Nov 04 '16 at 15:08
  • If I shouldn't put the Server Key into my javascript, and the client-side API Key isn't intended to work in this case, is there a recommended solution/practice? Can you obscure/encrypt a Server Key? – Derek Nov 04 '16 at 15:15
  • The Cloud Messaging Server Key is what I got the function to work with. The Web API Key did not work. – Derek Nov 04 '16 at 15:16
  • I asked around and indeed: this will only work with client-side API keys for older projects. For newer projects a server key is needed, which means that you can't safely subscribe to topics from a browser. I'll update my answer. – Frank van Puffelen Nov 04 '16 at 17:39
  • So how can we do it without showing the server key? – Sinan Noureddine Oct 18 '18 at 17:27
  • @FrankvanPuffelen any idea why the native app firebase sdk can subscribe to a topic via the client sdk but the web can't? – Titan Dec 30 '19 at 12:09
  • 2
    Isn't the link 'https://iid.googleapis.com/iid/v1' for GCM? Does it also work for FCM? – J. Doe Jan 01 '21 at 16:26
  • @J.Doe **https://iid.googleapis.com/iid/v1** is for GCM not FCM. Currently REST API for FCM [document here](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages), [or here](https://firebase.google.com/docs/cloud-messaging/js/send-multiple#subscribe_the_client_app_to_a_topic) have no subscribe to topics but only send messages. And the **Server Key** is currently not exists anywhere in Firebase console. – vee Dec 11 '22 at 07:51
10

Any one looking for php solution find below since you are going to use Api key of server so don't do this on client side

client side fire base js code

// Initialize Firebase
var config = {
    apiKey: "xxxx",
    authDomain: "yyy",
    databaseURL: "zzzz",
    projectId: "aaaa",
    storageBucket: "bbbbb",
    messagingSenderId: "ccc"
  };
firebase.initializeApp(config);

const messaging = firebase.messaging();

messaging.requestPermission()
.then(function() {
  console.log('Notification permission granted.');
  return messaging.getToken();
})
.then(function(token) {

//send this token to server
  console.log(token); // Display user token
})
.catch(function(err) { // Happen if user deney permission

  console.log('Unable to get permission to notify.', err);
});

messaging.onMessage(function(payload){
    console.log('onMessage',payload);
})

server side code by php curl

$headers = array
    ('Authorization: key=' . API_ACCESS_KEY,
    'Content-Type: application/json');

$ch = curl_init();
// browser token you can get it via ajax from client side
$token = 'drVdtCt82qY:APA91bEZb99GvoS9knv-cp5ThVBiYGBjUwl_Ewj2tKaRFwp7HoG347utaNKbgLWmkxpGadABtIg-DspPUh5sC_bc2JrBKVw10Ejg72nYxZgD2wBU-adYJo0yi03lX22s5K2UEp6gwnMv';
curl_setopt($ch, CURLOPT_URL, "https://iid.googleapis.com/iid/v1/$token/rel/topics/testIshakTopic");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, array());
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
echo "The Result : " . $result;

Hope it will help for PHP developers

Ishak Ali
  • 340
  • 4
  • 10
8

Franks solution is still valid. However you need to have a server that does the subscribe for you.

function subscribeTokenToTopic(token, topic) {
  fetch('https://myserver.com/'+token+'/rel/topics/'+topic, {
    method: 'POST',
    headers: new Headers({
      'Authorization': 'Bearer '+ oauthToken
    })
  }).then(response => {
    if (response.status < 200 || response.status >= 400) {
      throw 'Error subscribing to topic: '+response.status + ' - ' + response.text();
    }
    console.log('Subscribed to "'+topic+'"');
  }).catch(error => {
    console.error(error);
  })
}

then your server has a rest call something like this:

(java spring)

@RequestMapping(value = "/{token}/rel/topics/{topic}", method = RequestMethod.POST)
public ResponseEntity<?> subscribeTokenToTopic(@PathVariable("token") String token, @PathVariable("topic") String topic)  throws ServletException {
  URL url = new URL("https://iid.googleapis.com/iid/v1/"+token+"/rel/topics/"+topic);
  // make https post call here.. 
  ...
}

Hopefully that makes sense.

user7582859
  • 81
  • 1
  • 1
5

    import firebase from 'firebase/app';
    import 'firebase/messaging';

    const config = {
        apiKey: "xxxx",
        authDomain: "xxx",
        databaseURL: "xxx",
        projectId: "xxx",
        storageBucket: "xxxx",
        messagingSenderId: 'xxxxx',
        appId: 'xxxxx',
        measurementId: 'xxxxxx'
      };
      firebase.initializeApp(config);
        try {
              if (firebase.messaging.isSupported()) {
                const messaging = firebase.messaging();
                messaging
                  .getToken({
                    vapidKey: VAPID_KEY
                  })
                  .then((currentToken) => {
                    if (currentToken) {
                                           subscribeTokenToTopic(currentToken,FirebaseAdminTopic);
                    }
                  })
                  .catch((err) => {
                    console.log('Error to get token', err);
                  });

                messaging.onMessage((payload) => {
                  console.log(payload.notification)
                });

                // Otherwise, we need to ask the user for permission
                if (Notification.permission !== 'granted') {
                  Notification.requestPermission();
                }
              } else {
                console.log('firebase messaging not supported');
              }
            } catch (err) {
              console.log(err);
            }
            
            
            
        function subscribeTokenToTopic(token, topic) {
          fetch(`https://iid.googleapis.com/iid/v1/${token}/rel/topics/${topic}`, {
            method: 'POST',
            headers: new Headers({
              Authorization: `key=${FCM_SERVER_KEY}`
            })
          })
            .then((response) => {
              if (response.status < 200 || response.status >= 400) {
                console.log(response.status, response);
              }
              console.log(`"${topic}" is subscribed`);
            })
            .catch((error) => {
              console.error(error.result);
            });
          return true;
        }

 
Israt Jahan Simu
  • 1,040
  • 13
  • 7
0

Use the server key instead of config.apiKey is working fine.

selva
  • 1