We're excited to announce the full speaker list and agenda for the first ever Firebase Dev Summit!
Find the latest schedule on our newly launched site here featuring sessions such as How to Develop Rock Solid Apps with Firebase, a Firebase Analytics deep dive, Develop Mobile Apps without Infrastructure , and more!
The Firebase Dev Summit is on Monday, November 7th with a full day of talks, codelabs and office hours. The Summit will end with an afterparty where you can enjoy drum and bass and mix and mingle with event attendees and the Firebase team.
Tickets are sold out, but you can sign up for the waitlist to be notified if a spot becomes available. We will also livestream the entire event in case you want to join us online. Sign up to receive an receive an email alert when sessions are about to start!
We want to hear from you! Join the conversation on Slack, G+ and Twitter.
Hey there, Firebase Developers!
We wanted to let you know that we've made some new updates to the Firebase libraries for iOS, Android, as well as the Javascript SDK. You can read the full release notes here, but let's give you a quick summary of some of the new features you should be aware of:
You can read our release notes for iOS, Android, and web platforms to get all the details. Or you can just start playing around with the new libraries. That's fun, too.
Happy hacking!
Private Backups is a good solution for backing up, scripting, and gathering analytics by regularly exporting your entire Database. Previously, setup involved a bit of back and forth between the user and Firebase Support. We are now offering a self-service user interface for Blaze customers in the Firebase console that seamlessly enables daily backups of your Firebase Database to a Google Cloud Storage bucket.
To get started, visit the new “Backups” tab in the Database section of the Firebase console, and the wizard will guide you through setting up your automated backups.
To save on storage costs, we enable Gzip compression by default, and you can choose to enable a 30-day lifecycle policy to have backups older than 30 days automatically purged.
When the setup process is complete, you will have a Google Cloud Storage bucket. On a daily basis, we will backup your Database’s application data and rules in JSON format to your bucket. You can view the status and history of all your backups directly in the Firebase console. There is also a “Manual Backup” button which lets you instantly enqueue a current backup of your database and rules, and is incredibly useful to take specific timed snapshots or as a safety action before you perform any code changes.
Here are three good reasons to start using automated backup today:
Offline Scripting
Automatic Database Backups let you crawl, analyze, and perform large scripted actions against your data without affecting the realtime performance of your customers connecting to the Database.
Historical Data Analysis
With historical snapshots of your database, you can retroactively analyze your data, look for trends, and make product decisions based on past data points.
Recovery From Corrupt or Lost Data
With Automatic Database Backups, you can easily restore your data in the event of data corruption or loss. To restore your database, simply download1 a backup file to your local machine from the backups tab. You can then click the "Import JSON" button2 under the data tab to restore your Database to a previously backed-up state.
We are continuing to streamline the experience for Firebase developers, and this new release gives developers more control over their data while reducing the need to go through support processes.
With Gzip enabled, you will need to decompress your database data before importing back into Firebase. ↩
For very large Databases, it is more efficient to contact support for data restoration from our own regular backups, rather than through the JSON files of Automatic Database Backups. ↩
Today we're announcing web support for Firebase Cloud Messaging (FCM) with the release of a JavaScript library. This extends our current browser support, enables a dramatically simpler implementation process, and brings powerful features such as Topics and Device Group Messaging to the web.
Notifications are one of the most compelling tools for developers to build engaging experiences. Since we introduced the technology in Chrome, we've seen tremendous adoption, with more than 10B notifications being sent per day to websites. However, developers often tell us that implementing this feature on the Web can be challenging and that they want to access the same advanced features of FCM that are available on native notifications.
Firebase Cloud Messaging is a powerful system that already supports sending messages to iOS apps, Android apps, and Chrome. Starting today, developers can use FCM to send messages to browsers that support the Push API, allowing you to go beyond Chrome and also send to Firefox, Opera and others.
It is easier than ever to send notifications to your web users with the FCM JavaScript library, as FCM handles complex server-side features such as payload encryption and client-side features such as service workers.
You can use a default service worker implementation to get started quickly, and when you are ready to extend and override it, you can do so easily. In addition to this, when you’re using the FCM APIs, our servers can manage payload encryption for you. FCM users don't need to change a thing in their server implementation to achieve this!
However, the technical aspect of web notifications is just a start. In order to make the most out of web notifications, you need to engage your users with the right content in the right manner. Check out our “What Makes a Good Notification?” post for best practices on notification content and “Best Practices for Push Notifications Permissions UX“ post for tips on interacting with Web users to get permission for sending notifications.
Beyond providing an easier client implementation, the FCM JavaScript library also brings important FCM features to the Web.
With the FCM JavaScript library, you can send web push notifications to single devices, topics or groups of devices. With the addition of topic support on the Web, we are making it possible for developers to send a message to their Android, iOS and Web users who have opted in to a particular topic. To take advantage of topics and device groups, you can use the server-side APIs to manage your topics and groups subscriptions.
Currently the FCM JavaScript library enables developers to reach browsers that have Push API support. Namely:
Microsoft Edge has announced plans to support the Push API and Samsung Browser will be covered once they have message payload support. This coverage will increase over time as more browsers introduce support for service workers. Make sure to check out our release notes for updates!
Just follow our Getting Started guide, and make sure to checkout our Firecast video!
We have been working with early adopters to test, refine, and unleash the power of the new FCM JavaScript library, and create the best possible Web notification experience for users. Below are some of their success stories:
Alibaba.com, the leading wholesale marketplace connecting overseas buyers with suppliers in China, implemented our solution in two days and saw 4X higher engagement for users who receive web notifications compared with users who visit the website directly. See the full Alibaba.com case study.
AliExpress, a global retail marketplace, saw a 93.4% higher open rate on the web compared to their app notifications, and a 178% higher conversion compared to mobile site users who do not receive notifications. The AliExpress team is continuing to expand their uses cases. For instance, to promote sales on 11th November (also referred to as “Double 11”) - a festival commonly celebrated by young Chinese singles and also one of the biggest online shopping events in the world - AliExpress will be using FCM to send notifications on its website to remind users to take advantage of discounts on items they are interested in. See the full AliExpress case study.
Settle Up, a fast-growing startup that helps users track shared expenses, wanted to send notifications to users when changes were made to a bill. After implementing in a day, they began to see 37% higher engagement for users who receive web notifications. Moreover, as a user of Firebase Analytics, Crash Reporting, Hosting, and Test Lab, Settle Up was able to easily access all their tools through Firebase as a unified solution. See the full Settle Up case study.
Firebase Cloud Messaging is part of the Firebase platform and is available for free. We are excited to announce this launch and can’t wait to hear what you think!
Firebase Authentication supports four federated Identity Providers out-of-the-box, making it super easy to authenticate with Google, Facebook, Twitter and GitHub. For example, in a web app all you need to sign in your Firebase users with Google is:
var google = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithPopup(google);
However you may be interested in using other Identity Providers to let your users sign into your Firebase app. For example, Instagram can be a nice alternative especially if you plan on also using the Instagram API to let users share their Instagram photos.
Using Identity Providers for which Firebase doesn’t have built-in support is possible, but requires a bit more code, and a server. Let’s walk through the steps required to integrate Instagram Sign-In in a Firebase web app. Instagram uses OAuth 2.0 for sign-in, so this post should help you integrate with other OAuth 2.0 identity providers such as LinkedIn as well.
Instagram supports OAuth 2.0 as its main way to authorize apps and access user data which includes the Instagram user’s identity. You need to have the user go through the OAuth 2.0 auth code flow and grant access to your app. Here is how the OAuth 2.0 flow goes:
First the user needs to be redirected to the authorization endpoint of Instagram which will present the user with a consent screen the first time they are asked to grant access to your app. We’ll do that in a popup window.
After authorizing your app, users will be redirected back to your domain with an auth code. The auth code can be exchanged server side for an access token using your Instagram app’s credentials. In the process of exchanging the auth code, Instagram will also return the user identity (this sometimes requires an additional request with other OAuth 2.0 providers like LinkedIn).
On the server, once we have fetched the Instagram user information, we’ll create a Firebase custom auth token. This token will allow the user to sign-in into Firebase in our web app using the signInWithCustomToken method.
signInWithCustomToken
We’ll also pass the user profile information that we got from Instagram such as the display name and the photo URL so that we can update the Firebase profile on the client - Note: Instagram does not provide the email of the user so we we’ll have a Firebase account without an email, which is fine. Once done we close the popup and Voila! your user is signed into Firebase with profile data from their Instagram account.
Let’s now go into a bit more details and look at how to implement the key points of this integration. We’ll be writing our backend in Node.js.
You’ll need to add a button on your web app which starts the Instagram auth flow. Before we do this you’ll first need to register your application on the Instagram Developers console so that we can get your app credentials which are needed for OAuth 2.0.
In your Instagram app’s configuration make sure you have whitelisted http://localhost:8080/instagram-callback (for testing) and https:///instagram-callback (your production domain) as valid redirect URIs. Now take note of your Instagram Client ID and Client Secret. You’ll need them for later.
http://localhost:8080/instagram-callback
https:///instagram-callback
This is a typical step you’ll have to make with every OAuth 2.0 providers: register your app and whitelist callback URLs in return for a Client ID and Secret.
On the server we’ll be using the simple-oauth2 package which helps with hiding the details of the OAuth 2.0 protocol. You need to provide a few values to set this up such as your Instagram Client ID and Secret and the Instagram’s OAuth 2.0 token and authorization endpoints. Here are the values you need to use for Instagram:
// Instagram OAuth 2 setup const credentials = { client: { id: YOUR_INSTAGRAM_CLIENT_ID, // Change this! secret: YOUR_INSTAGRAM_CLIENT_SECRET, // Change this! }, auth: { tokenHost: 'https://api.instagram.com', tokenPath: '/oauth/access_token' } }; const oauth2 = require('simple-oauth2').create(credentials);
On your server, add a URL handler that redirects the user to the Instagram Consent screen. As part of this, you’ll need to provide a Redirect URI which is where the user will be redirected back to after going through the Instagram auth flow. In our case we’ll use /instagram-callback as our callback handler path.
/instagram-callback
app.get('/redirect', (req, res) => { // Generate a random state verification cookie. const state = req.cookies.state || crypto.randomBytes(20).toString('hex'); // Allow unsecure cookies on localhost. const secureCookie = req.get('host').indexOf('localhost:') !== 0; res.cookie('state', state.toString(), {maxAge: 3600000, secure: secureCookie, httpOnly: true}); const redirectUri = oauth2.authorizationCode.authorizeURL({ redirect_uri: `${req.protocol}://${req.get('host')}/instagram-callback`, scope: 'basic', state: state }); res.redirect(redirectUri); });
Also, to avoid session fixation attacks, pass a random string in the state parameter of the OAuth request and also save it as an HTTP cookie. This will allow us to compare the state parameter that we get back with the one saved in the cookie and make sure the flow originated from the app.
On the client, add a button which triggers a popup like this:
function onSignInButtonClick() { // Open the Auth flow in a popup. window.open('/redirect', 'firebaseAuth', 'height=315,width=400'); };
When a user clicks the sign-in button, a popup is opened and the user is redirected to the Instagram Consent Screen:
Upon approval the user is redirect back to the /instagram-callback URL handler with an Authorization Code passed in the code URL query parameter and the state value that we passed earlier.
code
state
Once the user has been redirected back to the callback URL we:
app.get('/instagram-callback',(req, res) => { // Check that we received a State Cookie. if (!req.cookies.state) { res.status(400).send('State cookie not set or expired. Maybe you took too long to authorize. Please try again.'); // Check the State Cookie is equal to the state parameter. } else if (req.cookies.state !== req.query.state) { res.status(400).send('State validation failed'); } // Exchange the auth code for an access token. oauth2.authorizationCode.getToken({ code: req.query.code, redirect_uri: `${req.protocol}://${req.get('host')}/instagram-callback` }).then(results => { // We have an Instagram access token and the user identity now. const accessToken = results.access_token; const instagramUserID = results.user.id; const profilePic = results.user.profile_picture; const userName = results.user.full_name; // ... }); });
We’re now done with the OAuth 2.0 specific part of this implementation and what we’ll do next is mostly Firebase specific.
Next we’ll create a Firebase custom auth token and serve an HTML page that will sign the user in using the custom auth token and update the Firebase user profile (more about that later).
app.get('/instagram-callback', (req, res) => { // ... }).then(results => { // We have an Instagram access token and the user identity now. const accessToken = results.access_token; const instagramUserID = results.user.id; const profilePic = results.user.profile_picture; const userName = results.user.full_name; // Create a Firebase custom auth token. const firebaseToken = createFirebaseToken(instagramUserID); // Serve an HTML page that signs the user in and updates the user profile. res.send( signInFirebaseTemplate(firebaseToken, userName, profilePic, accessToken))); }); });
To create a Firebase custom auth token, you’ll need to setup Firebase with a service account credential. This is what gives you administrative privileges required to mint these tokens. Make sure you save the service account credentials file as service-account.json:
service-account.json
const firebase = require('firebase'); const serviceAccount = require('./service-account.json'); firebase.initializeApp({ serviceAccount: serviceAccount });
Minting a custom auth token is simple, you just need to choose a uid for the user based on its Instagram’s User ID:
function createFirebaseToken(instagramID) { // The uid we'll assign to the user. const uid = `instagram:${instagramID}`; // Create the custom token. return firebase.auth().createCustomToken(uid); }
Note: Because service account credentials must be kept secure, creating custom tokens should always be done server-side.
Once we have the custom token we can pass it to the client to sign-in into Firebase.
At this point the server will serve an HTML page that is ran inside the popup window and will:
One trick is to use a temporary Firebase App instance to update the profile instead of using the default Firebase app. This prevents the Auth listener on your main page to trigger before the user’s profile has been updated:
app.get('/instagram-callback', (req, res) => { // ... // Serve an HTML page that signs the user in and updates the user profile. res.send( signInFirebaseTemplate(firebaseToken, userName, profilePic, accessToken))); }); }); function signInFirebaseTemplate(token, displayName, photoURL, instagramAccessToken) { return ` <script src="https://www.gstatic.com/firebasejs/3.4.0/firebase.js"></script> <script src="promise.min.js"></script><!-- Promise Polyfill for older browsers --> <script> var token = '${token}'; var config = { apiKey: MY_FIREBASE_API_KEY, // Change this! databaseURL: MY_DATABASE_URL // Change this! }; // We sign in via a temporary Firebase app to update the profile. var tempApp = firebase.initializeApp(config, '_temp_'); tempApp.auth().signInWithCustomToken(token).then(function(user) { // Saving the Instagram API access token in the Realtime Database. const tasks = [tempApp.database().ref('/instagramAccessToken/' + user.uid) .set('${instagramAccessToken}')]; // Updating the displayname and photoURL if needed. if ('${displayName}' !== user.displayName || '${photoURL}' !== user.photoURL) { tasks.push(user.updateProfile({displayName: '${displayName}', photoURL: '${photoURL}'})); } // Wait for completion of above tasks. return Promise.all(tasks).then(function() { // Delete temporary Firebase app and sign in the default Firebase app, then close the popup. var defaultApp = firebase.initializeApp(config); Promise.all([ defaultApp.auth().signInWithCustomToken(token), tempApp.delete()]).then(function() { window.close(); // We’re done! Closing the popup. }); }); }); </script>`; }
After the user is signed-in into the default Firebase app inside the popup, the auth state listener will trigger on your main page (remember, with Firebase the auth state is shared across tabs) and voila! You can display the user profile information, use the Realtime Database, Firebase Storage, etc...
We’ve created a demo app which you can try at: https://instagram-auth.appspot.com/
The sample is open source, feel free to have a look at the resources on Github: https://github.com/firebase/custom-auth-samples
The code shown in this article so far will work for your web apps. There are a few techniques to add Instagram auth to your Android or iOS app and we won’t cover them in this post, but stay tuned!
Let us know in the comments, or using the GitHub repo issues if you are looking for samples for other Identity providers or if you’re having problems integrating with them and we’d love to help!
One of my favorite features of Firebase Remote Config is its ability to deliver different content to different groups of users. For instance, you can change the look and feel of your storefront for people who have spent a lot of money in your app. Or you could emphasize one part of your fitness app for runners and another for weightlifters.
Ever since Remote Config first launched, you could accomplish some pretty sophisticated user targeting by delivering different content to people who were in different Firebase Analytics audiences.
But more recently, Remote Config added the ability for you to send different sets of data to people with different user properties, which has made this feature even more useful.
"Wait a second," you might be saying. "I can already target users with audiences, which allow more sophisticated targeting than just a user property. Why is this any better?"
And it's true; audiences give you some very powerful and very specific user targeting by enabling you to create groups of users who can be defined by a number of different events and user properties. For instance, you could create an audience of "Left-handed Canadians who have completed level 5 in my game."1
But audiences have two limitations that can make them more difficult to use within Remote Config:
First off, you're limited to 50 audiences, and they're shared among people who might be using these same audiences for other targeting features within Firebase, such as running Analytics reports, or building remarketing campaigns via Google AdWords. This makes it more difficult to create several smaller ad hoc user groups, or to edit existing audiences that other people in your organization might be using.
Second, with the way audiences currently work, once a user joins an audience, they can never leave. To your average marketer who uses audiences for remarketing campaigns, this might be exactly what they're expecting, but for Remote Config purposes, this can sometimes be problematic.
Imagine you wanted to create a "Newbies" audience of people who have started your game but not yet completed level 10. If you tried creating an audience out of this group, everybody who started your game would be placed into this audience. But then they would remain in this audience even if they reach level 15 or 20, essentially turning this into an "All players" audience.
And that's why, when I'm targeting users in Remote Config, I prefer to use User Properties as a way to personalize my content. They allow me to create lots of smaller one-off targeting groups, and let me be little more dynamic than with traditional audiences.
For instance, let's say you have a fitness app and wanted to deliver a different front page image based on the user's favorite fitness activity. If you have that favorite exercise stored as a user property, you can easily set that up by creating a new condition based on that property.
Then you can show a different front page image based on each one of those conditions.
In this way, you could easily create a half dozen different conditions and not worry about "using up" those Firebase Analytics audiences.2
Or imagine you have a game and you want to change aspects your game's behavior based on what level the user is at. You can do that by storing that level as a user property and then creating a number of different conditions based on these tiers. For instance, I can create a "intermediate" tier of players who are in between levels 4 and 10…
...and give a different daily bonus to those players.
As users progress in level, Remote Config will automatically start delivering them different values based on their level as their user property places them in different tiers. And I don't need to create new audiences for each one.
It's also simple to change these groupings later. If, in the future, I decide that my intermediate tier should really start at level 6, I can make that change within the Firebase control panel, and those changes are pushed out immediately to Remote Config.
Heck I could even add in a fourth tier if I suddenly decide I need to change my game's behavior for the the extra special players.
By default, user properties are stored as strings, which means you can run string comparisons like "exactly matches" or "contains" against them. If you stored your user's top 3 fitness activities as a pipe separated string (e.g. yoga|interval_training|running) you could create an "All runners" condition by targeting anybody whose fitness activities contained the string "running."
yoga|interval_training|running
But you can also run numeric comparisons against them, as we did in the playerLevel example above. Remote Config will translate strings to numbers as you might expect; "42" evaluates to 42, "3.14159" evaluates to 3.14159, and so on. Numeric comparisons with user property strings that don't translate to integers or floats (e.g. "Level_42") will always fail, however.
"42"
"3.14159"
"Level_42"
Being able to target user properties is a relatively new feature, so if you haven't played with it yet, I encourage you to give it a try. Head on over to the Remote Config panel and try making a minor change based on a user property you're already storing. Once you have that working, stop and think what parts of your app could really benefit from personalization, and considering adding another user property or two to support that.
And if you end up building something cool, let us know! We'd be excited to hear about it.
1Assuming you targeted "handedness" as a user property, that is.
2To be fair, there is also a limit to the number of Remote Config conditions you can create, but it's 100, which gives you a lot more room to experiment.
Let’s say you've got a chat app where you want to enable a private conversation among a select group of people. Or you have a photo sharing app where you want a group of friends to contribute to an album together. How could you limit this kind of data sharing to a small group of users without exposing it to the world?
This is where security rules in Firebase can help out. Firebase's security rules can be quite powerful, but they do sometimes require a little guidance. This isn't really because they're complicated, but mostly because most people tend to not use them frequently enough to build up a lot of expertise.
Luckily for all of you, I sit next to people who have built up some expertise in Firebase security, and I've been able to pester them over the last several weeks to get this blog post written. More importantly, I found this One Weird Trick that makes it easy to figure out security rules that I'll share with you… at the end of this article.
But for the moment, let's go back to our hypothetical example of a chat app that wants to have private group conversations. Anybody who's part of the chat group can read and write chat messages, but we don't want other people to be able to listen in.
Imagine that we've structured our database like so. There are lots of ways to do this, of course, but this is probably the easiest for demonstration purposes.
Within every semi-private chat, we have a list of people who are allowed to participate in the chat, along with the list of chat messages. (And yes, in real life, these userIDs are going to look a whole lot messier than user_abc.)
So the first security rule we want to set up is that only people in the members list are allowed to see chat messages. That's something we could create using a set of security rules like this:
{ "rules": { "chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()" } } } } }
What we're saying here is that you're allowed to read the chats in the chats//messages, as long as your userID exists in that same chat's members section.
chats//messages
userID
members
Curious about that $chatID line? That's kind of the equivalent of a wildcard that matches anything, but sticks the match into a $chatID variable that you could reference later if you want.
$chatID
So user_abc? Totally able to read chat messages. But user_xyz isn't allowed because there's no members/user_xyz entry within that chat group.
user_abc
user_xyz
members/user_xyz
Once we've done that, it's trivial to add a similar rule that says only members can write chat messages, too.
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).exists()" } } }
And we could get more fine-grained if we wanted. What if our chat app had a "lurker" user type, who was allowed to view, but not write messages?
"lurker"
That could be addressed as well. We want to change our rules to say, "You can write messages, but only if you're listed as an owner or a chatter." So we'd end up with something like this:
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'" } } }
(Note that for brevity, I've dropped the "rules" line from the rest of these samples.)
Incidentally, you might thinking to yourself, "Gosh wouldn't it be easier to just say 'Allow people to write chat messages only if they're not listed as a lurker'"? And sure, that would require one less line of code…
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() != 'lurker'" } }
...but quite often, when it comes to security, you're better off basing your security off of whitelists rather than blacklists. Consider this: What would happen if your app suddenly decides to add a new class of users (we'll call them "newbies") and you forget to update those rules?
With the first set of rules, that new group of users wouldn't be able to post anything, but with the second set of rules, that new group of users would be able to post whatever they want. Either case could be bad if it's not what you intended, but that latter case could be a whole lot worse from a security standpoint.
Of course, all of this is overlooking one tiny little problem: How were we able to populate those list of users in the first place?
Well, let's assume, for a moment, that a user is somehow able to get a list of their friends through the app. (And we'll leave that as an exercise for the reader.) There are a few options we can consider for adding new users to a group chat.
Frankly, any of these would work; it's really up to the app developer to decide what's the best user experience for their app.
So let's look at these in order.
To handle that first option, we'd need to set a rule that says "People who are already in the members list are allowed to write to the members list."
This is pretty similar to the rules we've already set up for posting to the members list:
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'" }, "members": { ".read": "data.child(auth.uid).exists()", ".write": "data.child(auth.uid).exists()" } } }
Essentially, we're saying that any user can read or write to the members list as long as the user's current user id already exists somewhere in that list.
Restricting this to just letting an owner write to the list is also easy.
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'" }, "members": { ".read": "data.child(auth.uid).val() == 'owner'", ".write": "data.child(auth.uid).val() == 'owner'" } } }
We're saying, "You can go ahead and write to the members branch of a chat, but only if your userID is already in there and you're listed as an owner."
So we've got that second case covered.
So what about the idea of allowing users to ask to join and then having the owner if the chat approve them? Well, for that, one good option would be to add a pending list in the database alongside the members list, where people could add themselves.
The group's owner would then be allowed to add these potential users to the members list and also delete them from the pending list.
So the first rule we want to declare is, "You can add an entry to the pending list, but only if you're adding yourself." In other words, the key of the item that's being added has to be your own user id.
The rule for adding this looks like the following:
"chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'" }, "members": { ".read": "data.child(auth.uid).val() == 'owner'", ".write": "data.child(auth.uid).val() == 'owner'" }, "pending": { "$uid": { ".write": "$uid === auth.uid" } } } }
Here, we're saying, "Go ahead and write anything you want to the pending/ branch, just as long as uid is your own userID."
pending/
If we want to be thorough, we can also specify that you can only do this if you haven't added yourself already to the "pending" list, which would look a little more like this:
"pending": { "$uid": { ".write": "$uid === auth.uid && !data.exists()" } }
While we're at it, let's also specify that you can't ask to add yourself if you're already a member of the chat. That would be kinda pointless. So we'd end up with rules like this:
"pending": { "$uid": { ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()" } }
Then we can add some rules to the overall pending folder that says an owner can read or write to it.
pending
"pending": { ".read": "data.parent().child('members').child(auth.uid).val() === 'owner'", ".write": "data.parent().child('members').child(auth.uid).val() === 'owner'", "$uid": { ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()" } }
And… that's about it! Assuming we've kept the rules from the previous section that only allow the owner of a chat to read or write to a members list, we've successfully added the security rules that allows an owner to remove an entry from the pending list, and add them to the members list.
Oh, but I guess we've forgotten one last, kinda-important rule: Allowing somebody to create a new group chat. How can we set that up? Well, if you think about it, you can add this by stating that "Anybody can write to a members list if that list is empty and you're setting yourself as the owner"
"members": { ".read": "data.child(auth.uid).val() == 'owner'", ".write": "data.child(auth.uid).val() == 'owner' ||(!data.exists()&&newData.child(auth.uid).val()=='owner')" }
For the purpose of this write, imagine you first start by writing to /chats/chat_345/members with an object of { "user_zzz" : "owner" }. That newData line is going to look at this object and make sure the child with the key of the signed in user (user_zzz) is listed as owner.
/chats/chat_345/members
{ "user_zzz" : "owner" }
(user_zzz)
Once you've done this, then you can go ahead and add whatever additional messages or users the owner wants. Since they are now officially listed as an owner, the security rules should allow those actions no problem.
Note that the security rules don't really have a concept of a separate "create directory" action. If a user is allowed to write to chat_456/messages/abc, that rule applies whether or not messages already exists. (Or, for that matter, chat_456.)
chat_456/messages/abc
chat_456
I am not a Firebase security expert, but I'm able to play one in blog posts. Mostly by running the rules simulator.
See, every time you make a change to the rules -- and before you publish them -- you can test out how they run by simulating reads or writes to the database. In the Rules section of the Firebase console, there's a "Simulator" button on the upper right that you can click on. This will bring up a form that allows you to test any kind of read or write action you'd like.
In this example, I'm testing out that last rule by having a user signed in as "user_zzz" attempting to add themselves as an owner to an empty /chats/chat_987/members list. The rules simulator is telling me this is allowed and highlighting the line where the write action evaluates to true.
"user_zzz"
/chats/chat_987/members
(Technically, it's highlighting the wrong line. It's the part of the rule in step 13 that evaluates to true. I think the highlighter doesn't handle line breaks within a string particularly well.)
On the other hand, if that user attempts to add themselves as an owner of a non-empty list, it fails, which is exactly what we want.
Note that there are a few other refinements that could be make here. Right now, we're set up such that owners can add other members as owners. That may or may not be what we want.
Come to think of it, we haven't done anything to validate that new members are being added with legitimate roles. And there's certainly some validation rules we could be adding to the chat messages to make sure they're of a length that our UI can handle. But perhaps this is an area you could play around with.
Copy-and-paste these final rules into your own version of a chat app and see what you can do to add these refinements.
{ "rules": { "chats": { "$chatID": { "messages": { ".read": "data.parent().child('members').child(auth.uid).exists()", ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'" }, "members": { ".read": "data.child(auth.uid).val() == 'owner'", ".write": "data.child(auth.uid).val() == 'owner' ||(!data.exists()&&newData.child(auth.uid).val()=='owner')" }, "pending": { ".read": "data.parent().child('members').child(auth.uid).val() === 'owner'", ".write": "data.parent().child('members').child(auth.uid).val() === 'owner'", "$uid": { ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()" } } } } } }
You're more than welcome to check out the documentation if you need more help, and play around a little with the simulator! I guarantee it'll the most fun you'll have this week playing with a database security rules simulator. Or at least in the top three.
We recently announced that the first Firebase Dev Summit will be held in Berlin on Nov 7th. A few spots for app developers are still available and we encourage you to register today!
In our experience there are a number of things required that make a great developer event- and it just happens we’ll have them all at the Firebase Dev Summit in Berlin next month:
1. Announcements. Since our announcement at I/O, we’ve worked hard to add even more exciting features that will help you develop and grow your app. Be the first to hear about our new releases and to exclusively try them out.
2. Meet the Firebase team. Firebase founders, product managers, and engineers will be on hand to answer questions, hear how you're using Firebase and discuss how we can make it even better.
3. Content. The full speaker list and schedule will be released next week! Spoilers: sessions on growth hacking with Firebase, app quality, developing without infrastructure, trainings for new features (and more!) will be part of the full day agenda.
4. Cool swag. This is our first ever Firebase Dev Summit and we take it pretty seriously…
5. Travel grant. We believe a diversity of attributes, experiences, and perspectives are needed to build tools and apps that can change the world. So, we’ve partnered with the Women Techmakers team to offer travel grants to women in technology interested in attending. Apply here to join us in Berlin.
6. Network. The Firebase Dev Summit is for app developers like you. This will be a great chance to meet other folks that are working on similar challenges, have a drink together and dance to the sounds of Drum & Bass while chatting about Firebase. :)
Oh, did we mention that the event is free of charge? We hope you can make it - don’t forget to reserve your spot before we sell out.
Can’t make it? Sign up here if you’d like to receive updates on the livestream and tune in live on November 7th.
This is the last post in this blog series about the Play Services Task API and its use in Firebase. If you missed the first three parts, consider jumping back to those before continuing here. When you're all caught up, let's finish up this series!
Throughout this series, we've only ever talked about units of work that are themselves represented by a Task or a Continuation. In reality, however, there are lots of other ways to get work done. Various utilities and libraries may have their own ways of performing threaded work. You might wonder if you have to switch to the Task API to unify all this if you want to switch to Firebase. But you certainly don't have to. The Task API was designed with the capability of integrating with other ways of doing threaded work.
For example, Java has always had the ability to simply fire up a new thread to process something in parallel with other threads. You can write code like this (though I heartily recommend against it on Android):
new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; // now figure out what to do with the result... } }).start();
Here we fire up that new thread from the main thread and do exciting work that ends with a String of interest. All that work that went into creating that string happens in parallel with the main thread, which continued executing after the thread was started. If that threaded work happened to block at any point, the main thread would not be held up by it. However, something must be done to get that String result into the place where it's expected. On Android, if that needs to be back on the main thread, you'll have to write more code to arrange for that to happen. This can get hairy. And we can use Tasks to help.
The Play Services Task API provides a way to make other units of work behave like Tasks, even if they weren't implemented as such. The class of interest here is TaskCompletionSource. This allows you to effectively create a Task "placeholder" that some other bit of code can trigger for success or failure. If you wanted that thread from above to behave like a Task without implementing it as a Task (as we learned last time by passing a Callable to Tasks.call()), you could do this:
final TaskCompletionSource<String> source = new TaskCompletionSource<>(); new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; source.setResult(result); } }).start(); Task<String> task = source.getTask(); task.addOnCompleteListener(new OnCompleteListener<String>() { ... });
We now have the thread offering its result String to the TaskCompletionSource using its setResult() method. Then, in the original thread, we simply ask the TaskCompletionSource for its "placeholder" Task, and add a listener to that. The result is now handled inside the listener running on the main thread. You can do the same in the failure case by calling the setException() method on the TaskCompletionSource. That will end up triggering any failure listeners, and they'll get a hold of the exception.
This strategy might seem a little bit silly up front, because there are less verbose ways of putting the result of some work back on the main thread. The value here is in the ability to work with that new placeholder Task along with other Tasks you might be working with in a unified fashion.
Imagine you're writing an app that absolutely depends on some values in Firebase Realtime Database, along with the values in Firebase Remote Config. However, to keep your users entertained while they wait for this data to load, you’d like to create a splash screen that shows some animation until that data is available to work with. Oh, and you don't want that screen to appear and disappear in a jarring way in the event that the data happens to be locally cached, so you want the screen to show for a minimum of 2 seconds. How might you implement this screen?
For starters, you'll need to create a new Activity and design and implement the views for the splash screen. That's straightforward. Then you'll need to coordinate the work between Realtime Database and Remote Config, as well as factor in the two second timer. You'll probably want to kick off all that work during the Activity's onCreate() after you create the splash screen views. You could use a series of Continuations to make sure all these things happen in serial, one after another. But why do that if you could instead start them all at once to run in parallel, and make the user wait only as long as it takes to complete the longest item of work? Let's see how!
The Task API provides a couple methods to help you know when several Tasks are all complete. These static utility methods create a new Task that gets triggered in response to the completion of a collection of Tasks that you provide.
Task<Void> Tasks.whenAll(Collection<? extends Task<?>> tasks) Task<Void> Tasks.whenAll(Task...<?> tasks)
One version of whenAll() accepts a Java Collection (such as a List or Set), and the other uses the varargs style of passing multiple parameters to easily form an array of any length. Either way, the returned Task will now get triggered for success when all the other Tasks succeed, and trigger for failure if any one of them fails. Note that the new Task result is parameterized with Void, meaning it doesn't contain any results directly. If you want the results of each individual Task, you'll have to get the results from each of them directly.
This whenAll() function looks pretty handy for knowing when all our concurrent work is done, so we can move the user past the splash screen. The trick for this case is to somehow get a bunch of Task objects the represent each thing we're waiting on.
The Remote Config fetch is easy, because it will give you a Task you can use to listen to the availability your values. Let's kick off that task and remember it:
private Task<Void> fetchTask; // during onCreate: fetchTask = FirebaseRemoteConfig.getInstance().fetch();
Realtime Database isn't as easy, because it doesn't provide a Task for triggering on the completion of available data. However, we can use the TaskCompletionSource we just learned about to trigger a placeholder task when the data is available:
private TaskCompletionSource<DataSnapshot> dbSource = new TaskCompletionSource<>(); private Task dbTask = dbSource.getTask(); // during onCreate: DatabaseReference ref = FirebaseDatabase.getInstance().getReference("/data/of/interest"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { dbSource.setResult(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { dbSource.setException(databaseError.toException()); } });
Here, we're registering a listener for the data we need to continue launching the app. That listener will then trigger dbTask to success or failure via dbSource depending on the callback it received.
Lastly, there's the minimum two second delay for the splash screen to stay up. We can also represent that delay as a Task using TaskCompletionSource:
private TaskCompletionSource<Void> delaySource = new TaskCompletionSource<>(); private Task<Void> delayTask = delaySource.getTask(); // during onCreate: new Handler().postDelayed(new Runnable() { @Override public void run() { delaySource.setResult(null); } }, 2000);
For the delay, we're just scheduling a Runnable to execute on the main thread after 2000ms, and that Runnable will then trigger delayTask via delaySource.
Now, we have three Tasks, all operating in parallel, and we can use Tasks.whenAll() to create another Task that triggers when they're all successful:
private Task<Void> allTask; // during onCreate(): allTask = Tasks.whenAll(fetchTask, dbTask, delayTask); allTask.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { DataSnapshot data = dbTask.getResult(); // do something with db data? startActivity(new Intent(SplashScreenActivity.this, MainActivity.class)); } }); allTask.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // apologize profusely to the user! } });
And that should do it! When the final allTask succeeds, we can do whatever we need with the data from the database, then we send the user to MainActivity. Without the use of Tasks here, this code becomes more tedious to write because you'd have to check the state of all the other ongoing units of work at the end of each of them, and proceed only when you know they are all done. Here, the Task API handles those details for you. And you can easily add more Tasks as needed without having to change the logic. Just keep adding Tasks to the collection behind allTask.
It's worth noting that there is a way to block the current thread on the result of one or more Tasks. Normally you don't want to block threads at all, if you can help it, but occasionally it's useful when you have to (such as with Loaders). If you do need to wait on the result of a Task, you can use the await() function:
static <TResult> TResult await(Task<TResult> task) static <TResult> TResult await(Task<TResult> task, long timeout, TimeUnit unit)
With await(), the calling thread simply blocks until the task completes, or the given timeout expires. If it was successful, you'll receive the result object, and if it fails, it will throw an ExecutionException which wraps the underlying cause. Please remember that you should never block the main thread! Only use this when you know you're running on some background thread, OK?
Here's what we covered in the four parts of this blog series:
This should be everything you need to know to make effective use of Play Services Task API! I hope you’re able to use Firebase along with the Task API to make efficient and delightful Android apps.