Back to home.

Store Push Notification keys on a React Native app at no cost using MongoDB App Services (Custom HTTP Endpoints from Data API) and Cloudflare Workers

by Angelo Reale

You might be on your journey to create your first mobile app, or might just be trying to improve an app you currently have. Maybe that’s not a commercial app, and you’re wondering: how can I store unique push notification keys that users generate on their own devices for sending push notifications to them at a low cost? Or better yet, at no cost?

I also asked myself the same, since the App I’m creating is just meant to be a hobbyist radio player for me and my friends, I didn’t want to spin up a fully fledged server just to store an array of a couple of strings like

1ExponentPushToken[XXXXXXXXX]
.

This is when I had the idea of using MongoDB App Services (disclaimer: I work at MongoDB but I don’t speak on behalf of the company) to create a simple HTTPS endpoint I could fetch from and create/read those push notification tokens.

If your app is not commercial and doesn’t have a lot of traffic (like mine) MongoDB App Services’ free tier can be rather generous (1M requests per month!), and I just needed to hit the create endpoint once in a user’s lifetime (the generated key is immutable over apps versions and re-installations), as well as hit the read endpoint every so often, when I’d need to actually send push notifications.

I was thinking, I didn’t want to expose the secrets to authenticate/connect to my MongoDB cluster out in the wild, so I’ve used Cloudflare’s Workers to intercept traffic on a specific wildcard endpoint of mine

1example.com/api/save_push_tokens?token=*
and do some Serverless request to the actual MongoDB App Services HTTPS Endpoint and add the token to my document.MongoDB App Services need authentication, so I’ve generated an
1api-token
for my Cloudflare layer, and used this method.

Also, the MongoDB’s Data API functions to create/read from the document are pretty darn simple, and I’ll share here:


1Create:
2exports = function({ query }, response) {
3    const {token} = query;
4
5    const doc = context.services.get("mongodb-atlas").db("YOUR_DB").collection("YOUR_COLLECTION").updateOne({}, {
6      "$addToSet": {
7        "tokens": token,
8      }
9    });
10
11    return doc;
12};


1Read:
2exports = async function({ query, headers, body}, response) {
3    const doc = await context.services.get("mongodb-atlas").db("YOUR_DB").collection("YOUR_COLLECTION").findOne({});
4    return  doc.tokens;
5};


Simple as that.


Then, on the Cloudflare side of things:

1addEventListener("fetch", (event) => {
2  event.respondWith(
3    handleRequest(event.request).catch(
4      (err) => new Response(err.stack, {
5        status: 500
6      })
7    )
8  );
9});
10/**
11* Many more examples available at:
12*   https://developers.cloudflare.com/workers/examples
13* @param {Request} request
14* @returns {Promise<Response>}
15*/
16async function handleRequest(request) {
17  const apiKey = 'YOUR_API_KEY'
18  let query
19  try {
20    query = request.url.split("?data=")[1].split("&")[0]
21  } catch (e) {
22    return respond({
23      error: e
24    })
25  }
26  if (!query.includes("ExponentPushToken")) return respond({
27    error: 'WRONG!'
28  })
29  const settings = {
30    method: "GET",
31    headers: {
32      'api-key': apiKey
33    }
34  }
35
36  const endpoint = `https://YOUR_MONGODB_DATA_API_ENDPOINT/?TOKEN=${encodeURIComponent(query)}`;
37  const res = await fetch(endpoint, settings)
38  return respond({
39    query, ok: res.ok
40  })
41}
42async function respond(payload) {
43  let response = new Response(JSON.stringify(payload), {
44    status: 200,
45    statusText: 'OK'
46  })
47
48  return response
49}


If you’re curious about the React Native side of things, I don’t mind also sharing some code (please highlight to the usage of

1@react-native-async-storage/async-storage
on the user’s mobile so we don’t blow up Cloudflare’s and MongoDB’s free tier limits!):

1import { StyleSheet, Text, View, Platform, Linking } from 'react-native';
2import Player from './components/Player';
3import { Provider } from 'react-redux';
4import { legacy_createStore as createStore } from 'redux'
5import playerReducer from './redux/PlayerReducer';
6import { useEffect, useRef, useState } from 'react';
7import * as Device from 'expo-device';
8import * as Notifications from 'expo-notifications';
9import axios from 'axios';
10import AsyncStorage from '@react-native-async-storage/async-storage';
11
12const store = createStore(playerReducer);
13
14export default function App() {
15  const [expoPushToken, setExpoPushToken] = useState(undefined)
16  const responseListener = useRef();
17
18  // store key
19  const storeLocalPush = async (value) => {
20    try {
21      await AsyncStorage.setItem('@push', value)
22    } catch (e) {
23      console.error(e)
24    }
25  }
26
27  const getLocalPush = async () => {
28    try {
29      const value = await AsyncStorage.getItem('@push')
30      if(value !== null) {
31        return value
32      }
33    } catch(e) {
34      console.error(e)
35    }
36  }
37
38  // PUSH
39  const registerForPushNotificationsAsync = async () => {
40    if (Device.isDevice) {
41      const { status: existingStatus } = await Notifications.getPermissionsAsync();
42      let finalStatus = existingStatus;
43      if (existingStatus !== 'granted') {
44        const { status } = await Notifications.requestPermissionsAsync();
45        finalStatus = status;
46      }
47      if (finalStatus !== 'granted') {
48        alert('Failed to get push token for push notification!');
49        return;
50      }
51      const token = (await Notifications.getExpoPushTokenAsync({experienceId:'YOUREXPERIENCEID'})).data;
52      setExpoPushToken(token);
53    } else {
54      alert('Must use physical device for Push Notifications');
55    }
56
57    if (Platform.OS === 'android') {
58      Notifications.setNotificationChannelAsync('default', {
59        name: 'default',
60        importance: Notifications.AndroidImportance.MAX,
61        vibrationPattern: [0, 250, 250, 250],
62        lightColor: '#FF231F7C',
63      });
64    }
65  };
66
67  // store push
68  const storePush = async () => {
69    const local = await getLocalPush()
70    if(!local || local !== expoPushToken) {
71      const res = await axios.get(`https://YOUR_CLOUDFARE_READY_SERVICE_WORKER?data=${expoPushToken}`)
72      const ok = Boolean(res.data.ok)
73      if (ok) await storeLocalPush(expoPushToken)
74    }
75  }
76
77  //open site
78  const openSite = async (url) => {
79    try {
80      await Linking.openURL(url);
81    } catch (e) {
82      console.error(e)
83    }
84  }
85
86  useEffect(() => {
87    registerForPushNotificationsAsync()
88
89    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
90      const url = response.notification.request.content.data.url
91      if (!url) return
92      try {
93        openSite(url)
94      } catch(e) {
95        console.error(e)
96      }
97    });
98  }, [])
99
100  useEffect(() => {
101    if(expoPushToken) {
102      storePush()
103    }
104  }, [expoPushToken])
105
106  return (
107    <Provider store={store}>
108      <View style={styles.container}>
109        <Player />
110      </View>
111    </Provider>
112  );
113}
114
115const styles = StyleSheet.create({
116  container: {
117    flex: 1,
118    backgroundColor: '#fff',
119    alignItems: 'center',
120    justifyContent: 'center',
121  },
122});


As for the actual sending of push notifications, I’d do it every so often, so all I need is a simple node script:

1const fetch = require('node-fetch')
2const apiKey = 'YOUR_API_KEY'
3const endpoint = `YOUR_DATA_API_ENDPOINT`
4
5const title = `We're live!`
6const body = 'Get into the groove from the app.'
7const data = { url: '' }
8
9const sendPushNotification = async (expoPushToken) => {
10  const message = {
11    to: expoPushToken,
12    sound: 'default',
13    title,
14    body,
15    data
16  };
17
18  await fetch('https://exp.host/--/api/v2/push/send', {
19    method: 'POST',
20    headers: {
21      Accept: 'application/json',
22      'Accept-encoding': 'gzip, deflate',
23      'Content-Type': 'application/json',
24    },
25    body: JSON.stringify(message),
26  });
27}
28
29const getKeys = async () => {
30  const settings = { 
31    method: "GET",
32    headers: {
33      'api-key': apiKey
34    }
35  }
36  const res = await fetch(endpoint, settings)
37
38  if (res.ok) {
39    const data = await res.json()
40    console.log({ data })
41    return data
42  }
43  return []
44}
45
46const sendNotifications = async () => {
47  const keys = await getKeys()
48  keys.forEach((token) => {
49    sendPushNotification(token)
50  })
51}
52
53sendNotifications()


And, voilà!

Now you can start pestering your friends who have your app installed with push notifications for free! :)

My app’s first push notification! If you’re curious, visit purizu.com.