Zero to App: Live Coding a Cross-Platform App on Firebase (Google I/O'19)

[MUSIC PLAYING] MIKE MCDONALD: My name is Mike McDonald I’m a product manager on the Firebase team I’m here to talk today about everyone’s favorite topic How many people are here for business? Let’s start with that Your company is paying for you? Let’s imagine– you’ve been out all day in the sun It’s hot If you’re like me, maybe got a little sunburned You are now going to go to the local drugstore and pick up some aloe vera You pay for that, and you’re going to expense it, because the company’s paying for it You got sunburned And you have to deal with everyone’s favorite problem, which is expense tracking Right? You all love this process You get back from a really great trip out to I/O, and then you just have this cluster of emails, and cluster of receipts that you have to deal with You have to upload all those receipts, you have to scan them You have to determine how much it was You maybe want to know, oh god, am I spending more than everyone else on my team? Wouldn’t it be awesome if I went on a trip, and I had a receipt– you can check out my burgers and beer from a trip up to Seattle I want to just take a photo of that, have some magic occur, and then know, hey, that cost me $51.74 dollars And have that automatically tracked That’s kind of my dream It would be even better if I could allow everyone on my team to do that Maybe I have an iPhone, and my co-worker has an Android Or maybe I want to access all of those things on the web, after the fact I don’t really want an app I need this app to be available across Android, iOS, and web If I just sat down and started building an app the way I normally built an app, not only would I build all of those front ends, but I would build a bunch of backend infrastructure I would have Compute Engine VMs, or maybe I’m using App Engine, or serverless things like Cloud Run or Cloud Functions But I’m writing a bunch of back end code, to do authentication and authorization Maybe I have an ORM in there to talk to the database that I’m also managing It’s kind of a huge pain It’s a ton of infrastructure, overhead, and all I want to do is upload receipts and scan them It should be easy I’m going to use Firebase to help me build this application Firebase is Google’s mobile platform It helps you develop your applications, understand what’s going on in your application to ensure that it’s high quality, and then helps you grow your application Firebase apps are different, because they don’t require that you build all of that infrastructure It’s a bunch of managed services that let you do things like upload files directly from your phone to the cloud, or synchronize data across devices We’re going to start off I’m going to invite a couple of my friends out Frank, Jen, and Kat– I need some help building this app [APPLAUSE] FRANK VAN PUFFELEN: Thanks, Mike We’re going to build a few apps here I’m going to give the team a few moments to set up We’re going to talk through what we’re actually going to be doing Mike already said, we’re building an expense tracking app Who’s excited about that [AUDIENCE WOOS] FRANK VAN PUFFELEN: Not too bad I’m not sure about you, but when I am out having dinner– when I’m traveling– when I get the receipt, I want to take a picture straight away I grab my phone, and I take a picture I never actually do my expenses for my phone I file my expense reports from my laptop, in a browser I have the same app on my phone– typically an Android version for me– and in the browser, a web version I need to see the same receipt images there We need to store them somewhere where we can access the same files from any version of the app We’re going to use Cloud storage for that Cloud Storage is Google’s exabytes scale storage solution It’s used to back some of the biggest apps that you may have on your phone For example, Snapchat relies on cloud storage for its files That is great, because even though we’re just starting with the four of us today, this means that we know that the app will keep working even when there’s millions of users tracking their expenses We’re going to access cloud storage straight from our devices We’re going to read and write files to the cloud straight from within our application code We’re not going to spin up any servers We’re going to just use the Firebase SDK for Cloud Storage in each of our apps That means that we’ll write a file straight from the iOS app, and then later we can access that same file from within our web app because we’re using the Firebase SDK to access cloud storage We can share the file with users worldwide if we want, but since in this case we’re tracking expenses and we’re actually storing receipts, this is probably somewhat sensitive information We can also lock it down to just the users that we want to give access to these files I’ve been telling you that it’s very easy to actually write this code How about we actually write some code?

Can we switch to the split screen, please? As Mike says, we’re going to be building three apps here today We’re going to be building the expense tracking app for iOs, for Android, and for the web at the same time We’re going to do that by writing them in parallel We’ve split the screen into four pieces At the top left, you see that Mike is working on the iOS version of the app, and he’s doing that writing Swift On the top right, you see that Jen is working on the Android version of the app, and she’s writing Kotlin On the bottom left, you see that Kat is working on the web app, and she’s doing that in JavaScript Now each of them has already done some prep work on the app, because we don’t want to bore you with UI details and things like that If we run the [INAUDIBLE] now, we can see the prep work that we’ve done We’ll share the code for the entire app after the talk, so that you can see what we’ve done We’re not trying to hide anything, we’re just trying to save some time for things that are not relevant to what we’re talking about today We have a very basic app, with a very simple layout We can select an image, or take a picture If we do that, nothing happens, because we haven’t wired up Firebase yet That’s what we’re going to do next If you look at the bottom right, you see that we have the Firebase console This is where you manage all of your Firebase projects As you can see, I have a lot of them We already created a project that will serve as the back end for these apps All our apps are going to be talking to the Firebase backend services in this project You can see that on the left, we have lots of Firebase features that we can use from our apps, like storage that we’re going to start with, a database, authentication, functions I think that’s the ones that we’re using today We’ve already added information about each of the apps to this project We added information about the iOs, Android, and web application to the Firebase project so that it knows what apps it’s going to connect with Based on that, Firebase generates configuration files that we downloaded and added to the IDE We took Google services info.plist and we added it to xcode We took a Google services [INAUDIBLE],, and added it to Android Studio And we took a configuration snippet and added it to the web app With that, we’re ready to start coding From here on, I recommend that you follow along with the technology that you’re most interested in Top left, for iOS Top right, for android Bottom left, for web In each of these ideas IDEs, we already added the configuration data and the SDKs that we’re going to be using We’ve done a plot install We’ve done a gradle dependency, and we’ve included some script snippets Now, we’re really ready to start writing some codes Remember where we started off We took a picture– or selected a picture from the gallery– and we need to upload that to cloud storage When the user selects an image, this on image selected method gets called This is where we need two things We need a reference to the local data of the image that we just took, or selected And we need to know where in Cloud Storage we’re going to write this data We’re going to build a path into Cloud Storage– called a storage reference– out of three pieces We start with a fixed name called receipts, which is just a folder where we store all the receipts for all users If we ever need a different type of file, we would store it in a different top level folder Then under that, we create a folder with a user ID for each user that uploaded the receipt So Mike’s receipts are going to be in his folder, and Jen’s are going to be in hers Then, we end with a unique ID for the file name It’s really just a unique ID we generate to make sure we never have file name clashes Next up, we’re going to tell Firebase to start writing the data to Cloud Storage We do that by calling on the storage reference to put the data to Cloud Storage This is all we need to do to get Firebase to start uploading the files in the background And note, all the things we did not have to do here We did not have to check for network connectivity We didn’t have to spin up any background tasks, or threats We just told Firebase to start uploading the data, and it went to work Now when the upload completes, that can be one of two cases Either the upload succeeded, or it failed If the upload succeeds, we’re going to display a message that it succeeded, for the moment But when the upload fails, we take the error message that we got from Firebase, and we display it on the screen because this might be really useful if we need to troubleshoot something This is all that it takes Let’s build and run the application, and see what we actually can do now Now, if we select a picture, you can see that the upload starts and we get a very nasty error message in some of these devices

If you look carefully, it says that permission was denied We don’t have permission to write any files, which sort of makes sense But we didn’t talk about that yet So let’s switch back to slides, and see why this happens and how we’re going to fix it Remember when I said that if Mike uploads receipts, that probably only Mike should be able to see them And when I upload receipts, that only I should be able to see them That’s actually what’s happening here We store files in user specific folders that only the user can access, and we do that based on the user ID These are Firebase surface hide security rules We’ve written these, and these are automatically enforced on the server Because remember, we are accessing cloud storage directly from within our app So we can’t do security through surface hide code We have to do it through these rules And these rules tell us one very important thing In order to be allowed to read and write the receipts/userId folder, you must be signed in as an authenticated user with the same user ID That explains why nothing works yet, because we didn’t sign in yet Let’s fix that by adding Firebase authentication Firebase authentication is the second product for use, and it’s our secure serverless sign-in solution By just using the Firebase SDK in your application, you can allow your users to sign in to any of our supported providers This includes email passwords, phone number, maybe even a password word list link in email, and many social providers like Facebook, Google, Microsoft, Yahoo, Twitter, GitHub– I think we have a few more By just writing client side code, you can sign your users in with any of these providers When the user signs in, we generate a unique user ID for that user We do that the first time they sign in, when we create their accounts This ID will stay the same for the user, for as long as they’re using your application It’s the same, no matter what platform they sign in from So if today, Jen signs in on her Android app, and tomorrow she signs in on the web version of the same application, she gets the same user ID That is great, because that means we can access the same files that she wrote from her phone You do all of this without writing any surface hide code So no triple-legged OAuth validations You just write a few lines of client’s hide code, and Firebase does the rest There’s more to authentication than just code You actually also need UI You think of some of your favorite apps– you know that there’s a lot involved when you first signed into them You need to sign up, or sign in You need to enter your email address and your passwords, and wait As soon as you enter an email address, you probably want to send a verification email because if the user ever forgets their passwords, you need to send them a reset email All of these things require that you have a user interface for these actions, for these flows Let’s be honest, all of these flows have nothing to do with what your app is all about None of it is about expense tracking We’ve built a library called Firebase UI It’s built by the identity experts at Google, and includes years of best practices in building good sign-in flows This library is available for iOS, Android, and the web, and we’re going to be using that today to build our sign-in flow Let’s switch back to the codes to actually do that Can we switch back to the split screen, please? Just like before, we already included the Firebase authentication SDK We have a method here that gets called when the user clicks the sign-in button What we’re going to do is we’re going to add code to call out to Firebase UI to start a sign-in flow This is a single call to Firebase UI, but we’re passing a few parameters that tell Firebase how to actually display this The most important parameters are the providers that we want to enable So in this application, we’re going to allow the user to sign in with email and passwords, and with a Google account Now as I said before, we have many more providers, and if you want to enable those, you would just add them to this list You would also enable them in the Firebase console This is all that we need to do to kick off a sign-in flow See again how little code we have to write When the user completes the sign-in flow, it’s again going to be one of two states Either the sign-in succeeded, and in that case, we’re going to display the username Or the sign-in failed, and just like before if the sign-in failed we’ll take the error message that we get from Firebase, and we’ll display it to the user Let’s build and run these apps again, because I think this should be all that we need to do Now, if we click the sign-in button we get a pop-up that’s asking us to sign in We can select our account, or enter or email

password credentials If this is the first time that we sign into this app, we actually– for OAuth, we get a prompt whether we want to give the app access to our basic information When we sign in, we see the user’s display name With that, we should actually be ready to now upload an image Let’s see if this time, it succeeds That looks much better It’s a bit hard We don’t do anything with the image yet, so right now you have to take my word for it Let’s look at the Cloud Storage console in the bottom right, to see if any of our files actually uploaded Kat, can you go to the Cloud Storage console? You will see that we have a folder called receipts, just like in our code In there we have three sub folders Only two of them completed so far In each of these, we have a file of the receipts that they just uploaded If you look at the metadata, you can see that the time-stamp is really what just happened So our files are making it to Cloud Storage That’s a pretty good basic start for what we need for sign-in We’ve just signed into the app, we’ve taken a picture of a receipt, and we’ve uploaded it to Cloud Storage I think the next steps that Mike was talking about, is that we need to get some information from these receipts and then start storing that in a database Jen, how do you feel about actually doing that? JEN PERSON: Sure, I’ll give it a go Let’s go back to the slides We’ve stored the receipt images for our expenses in the Cloud now What’s next for our app? Right now, we just have a bunch of receipts that are sitting in Cloud Storage We don’t really show anything in our app We could ask the user to enter the details about the receipt themselves, manually, but this is Google I/O, and machine learning is all the rage So we really want a software solution to this We are going to use Google Cloud Vision to extract all the text from the receipt, find the total amount of money in there, and write that to the database We can’t use Cloud Vision right on our device, because that would give all of our users access to our API keys, to run their own Cloud Vision jobs, which we certainly do not want We could also use ML kit on iOS and Android But for this specific case, we really want to ensure that we know that the amount on the receipt is correct, so we don’t want that access to happen on the client What we really need is a trusted place where we can run code, that automatically gets called when we upload an image to Cloud Storage Then, calls Cloud Vision to get the amount for the receipt, and securely writes it to a database so that the app can read it To solve this, we’re going to use Google Cloud Functions Why use Cloud Functions, or let’s say, any server solution, or serverless solution There are several reasons where this would be an advantage For one thing, security In our case– and many other cases– there are things that you don’t want the client to be able to have access to You want to be able to process data in a place away from the client that can be secured Also, it’s really nice when you only have to write your business logic once and have it work across platforms A server or serverless environment enables us to do that And finally– and perhaps most important to your users, from their perspective– is if you’re doing something that is battery intensive, or data intensive, you really want to do that away from the client That’s going to be a better experience for them They’re not going to be too happy with an app that is draining their battery In this case, we could solve this by spinning up our own server, VM, container, but in this case we are working like a serverless microservice We need to run just a tiny bit of code in a trusted environment that automatically spins up and down as needed That’s where Cloud Functions comes in Cloud Functions are event driven, meaning that they respond to events that are happening in your app This could be a user logging in to Firebase Auth for the first time, writing a document to a database– triggering an analytics conversion event– or even triggering directly using an HTTP trigger Whatever one of these events occurs, your Cloud Function is activated and you can act on this information as necessary This could be all sorts of different things Maybe using Firebase Cloud messaging to send a notification to a user It could be uploading or changing some data in Cloud Firestore, or performing some modification

to Google Cloud Storage, or even Amazon S3 because it’s going to work with third party services, as well Cloud Functions are written in TypeScript and JavaScript, on top of no JS And thanks to NPM– the world’s largest repository of Node.js models– you have over 350,000 packages available at your disposal So there are lots of options for how to use Cloud Functions Cloud Functions automatically scale up and down to meet user demand, which means that even if your app is an over night success, we’re going to have you covered At the same time, you only pay for what you use So if you scale down to zero, you’re not going to pay anything Here you see Cloud Function scaling up and down Clound Functions work inside a privileged Google environment, which means that you can make use easily of Firebase and other Google Cloud Services, often without having to perform any additional configuration or setups stats You don’t have to worry about getting the credentials They are automatically provided by Google Let’s see what this looks like in code Let’s go back to the code Where you’re going to want to draw your attention is to the bottom right, because that is where our Cloud Functions code is Here we have a little bit of setup that we have initialized Let’s get right into the functions Our first step is going to be to create a Cloud Storage trigger, which has a format like this You’ll see functions whatever the Firebase product is, and this one is on finalize which means this code– whatever we put inside of here– is going to run anytime there’s is a change made to our cloud storage bucket The next thing we’re going to want to do is determine the user ID and file name We’re going to need this information when we we use Google Cloud Vision to extract the information from the receipt Let’s get back to that part, where we extract the information from the receipt That’s just going to give us any text that is in the receipt But what we really want is the total We have designed a custom function that searches the receipt, gets all that information, and looks specifically for the total amount I’m not going to go into what that does right now, but you will be able to check it out on GitHub if you’re interested in how that works There’s still one more step, because right now we have the information We have the total But we haven’t put it anywhere yet So we need a database in order to store it Let’s go back to the slides to talk a little bit more about that Here we are– we uploaded the receipt, we extracted the amount from it What comes next? It’s a good time to talk about a database Most apps these days need to store data, and our app is no different Right now, we’re just looking to store the total amount of how much a single receipt costs, but it’s possible that we’ll need more information in the future Maybe it could be a cost code, or maybe where it was spent, et cetera There’s a lot more information that we may need in the future For our database, we’re going to be using Cloud Firestore to store that information Cloud Firestore is a hosted NoSQL database that we can access directly from our app It comes with a rich client library that makes interacting with Cloud-based database easy, and it provides effortless syncing so you can check out changes as they are happening Cloud Firestore also has a robust offline mode, so your users can keep interacting with your app, even when data is not available If you don’t need real time syncing, you can still do one-time fetches Cloud Firestore is Cloud hosted, so we can share data between our users, which is exactly what we need And we can access Firestore directly from our application, meaning we don’t have to write any server side code to make the read that our app is going to do We just incorporate the Firestore SDK Cloud Firestore is a NoSQL database So if you’re familiar with SQL, and you’re used to rows and columns, we’re not going to have that structure here Instead, our structure is going to look a lot more like this What we have is documents, which have all sorts of fields in them where the key is a string and the value could be all sorts of different object types

These are grouped together in what we’re calling, collections Each of these represents a collection of documents Specifically, let’s zero in on what the structure is for our own application Here we have a collection of users and you’ll see their user ID We have some fields, user cost and team cost– sort of foreshadowing what we’re going to be talking about in the future Under each user, They have their own collection of expenses which lists out all of the expenses that they have uploaded Here’s a look at some of the rules that we would use to secure the database This allows us to access the expense documents A user can only read their own expenses in this case We don’t allow users to write directly because we’re doing that from our Cloud Function Speaking of which, let’s go back to the code so we can add that Cloud Firestore code Again, you’re going to be looking at the bottom right screen to check out the Cloud Functions What we’re going to do is write the total amount to Cloud Firestore, which looks like this It uses the admin SDK Whenever you have to do any sort of server side or serverless side work, you’re going to think of the admin SDK in order to access Firebase products We still have one more step to do We are writing to Cloud Firestore, but our applications are not reading from it Let’s take a look at our client apps for a moment, and see how they’re going to set this up Our first step is going to be to attach a listener That’s going to be updated any time a new expense is added This is going to read the most recent expense and then, we’re going to want to display it in the UI so that you can see it Let’s see if this works We’re going to run our apps again, add a new receipt, and see if we can see the most recent upload We still have a couple numbers there that aren’t listing any information We really want to know about some total amounts, so I’m going to pass things over to Kat, who’s going to be able to tell you more about that KAT FANG: Thanks, Jen At this point, we have our receipt stored in Cloud Storage, and we’ve pulled out the totals and saved them as Firestore documents This means I have all of the data that I need to get useful aggregate information, and answer questions like, how much am I spending? Since I have access to all of my documents, I could do this on the client side by pulling all of my expense documents for Firestore onto my device and summing up the total there Now, that works well if I have a couple of expense documents, but as I travel more the number of receipts I have will keep growing and I could end up with hundreds or thousands of entries And it can get a little expensive to pull down this many documents It’s not efficient to pull down this much data when all I want is a single value to answer, how much am I as an individual spending? Instead, we’ll optimize for read time How can we do this? We can use another Cloud Function to help determine the total spend for each user Just like we triggered a Cloud Function every time we uploaded a receipt, we can write another function that triggers every time an expense document is written to Firestore When we get a new expense for a user, we can update their user document to add the amount from that receipt to their running total in the user cost field Let’s take a look at what this looks like in code Again, we’ll be in the lower right hand screen First, we’ll need to create a new function We’ll call this calculate user cost This should trigger whenever a document gets written to Cloud Firestore by an earlier function We specify this by putting in our function a document path pattern This function will only trigger four documents that have an expense ID and are associated with a particular user by their UID When this triggers, we will get a snapshot of the data, along with some other event context Now we can read the amount from the document, and grab that UID from the document path

Using this information, and the admin SDK, we can get a reference to the user document that we actually want to update We use a field value dot increment, and add the new expense account to the running aggregate for the user We also keep track of the last updated time which we can get Firestore to fill in by using the server time stamp function You’ll see here we’re using merge true and set This allows us to either create or update the document, depending on whether or not it already exists And like before, we return a promise so that the function continues to run until the work to update the user document is done At this point, we deploy our function, and that will write the data to Firestore Now we just need to update the clients to read this data Let’s turn our attention back to the other three screens for each of our three client platforms Next to our existing listener– getting the latest expense we uploaded– we’ll add another listener However this time, we know exactly which user document we want to listen to Instead of using a query listener, we’ll use a snapshot listener that points to that particular document Then, we will pull out the user cost field that holds our individual total and update our UI Now if we go ahead and upload a new receipt, we’ll be able to see it upload, which will then trigger the function, which parses out the total And that will in turn trigger the function, which adds it to our own user total Awesome Now I know how much I’m spending as an individual But I’m also interested in the aggregate information of how much we are spending as a team We’ll be adding this to the user document, as well, so we only have one document that we need to read to get all of the aggregate information You’ll note in our UI code, I’ve already pulled this information out and we’re updating the UI with our team cost as well The last thing we’ll need is the actual team cost data itself Let’s go back to the slides to see how we can do this At this point, we have three apps running on three platforms, each scanning receipts and summing up how much we are individually spending Since we’re often traveling together, I want to know how much we’re spending as a total, just to make sure we’re not going over budget However, we can’t read each other’s expenses or user documents, so how are we going to be able to get this information? This is another place where Cloud Functions comes in handy As Jen mentioned, functions is trusted code, which means that it is allowed to access everyone’s documents even if I, as a user, cannot This time we’ll write a function that gets triggered whenever anyone’s user cost document gets updated If it does, we’ll calculate the delta and add that amount to the team cost build, and fan that data out to all the users so everyone has access to that information Our functions pipeline looks something like this When Jen uploads a receipt, that will trigger a function to use Cloud Vision to parse out the total, and store that as a Firestore document That in turn will trigger another function, which will update her aggregate user cost in her own user dock And that in turn will trigger another function, which will update the team cost for myself, and Frank, and Mike But, wait If I update the team cost, isn’t that in the user document as well? Wouldn’t that, in turn, trigger the function which would update everyone’s team cost again, which would end up in an infinite loop? Yes, it could So we’ll need to be able to detect this case, whether it was a user cost update– like in Jen’s case– or if we have updated the team cost, in which case we want to stop the potential infinite loop Let’s go ahead and take a look at what this looks like in code We’re in the bottom right hand corner We’ll need to define a new function We’ll call this one, calculate team cost We want this to get triggered anytime a user cost gets updated, which means we’ll want to write a trigger for on write as opposed

to on create, as we did in our previous function Where on create functions give us a snapshot of the data, and on write function gives us a change object, which will tell us what the data was before and after the write operation Having the changed data allows us to determine if this is the case where Jen’s uploaded a new expense, and we have new information to fan out to all of our users Or if it’s the case where Mike’s team cost’s got updated, which means no new data was generated, and there is nothing to update We can do this by getting the old and new user cost Old total here uses the change.before fields to find out what the user cost was before the write New total here uses change.afterfields to find out what the user cost is now If these two totals are exactly the same, that means the user cost field was not updated, which is the case of what happens when Mike’s document gets updated when Jenn uploads a receipt In that case, we don’t need to update anyone’s information, and our work here is done We can return true to get out of the function Otherwise, there is an update to the user cost and we want to add this to everyone’s running team total To do this, we’ll need to loop over every user document We’ll use the admin SDK once more, using the get function to fetch all documents in the user collection This gives us a query snapshot, which we can loop over and update each document in turn Like with user cost, we’ll once again use the increment value to ensure that this amount gets added to the team cost transactionally Again, we want to make sure that the function doesn’t quit prematurely because all of these updates are happening asynchronously We’ll need to keep track of the promises returned for each one of these updates, and we’ll return promise.all, which will ensure the function waits for all updates before exiting Now, we’ve already updated the UI to pick up the team cost as soon as the data field is there Let’s try uploading a receipt one more time and see if we can get the team cost update across all three platforms You might notice that the team total is a bit smaller than expected This is because the function only picks up on new data, which means that earlier receipts are not calculated as part of this We could have another means of back filling this data However, that’s all that we have time for now, so if you have questions about this, find us in the Firebase area Mike, what do you think of our expense tracking app? MIKE MCDONALD: I just uploaded all of my receipts, so I think it’s awesome Can you go back to the slides, please? I came to you all about 38 minutes and 15 seconds ago with the problem of, I want to keep track of my expenses Frank walked us through how I can sign in, get an OAuth token, securely upload my photos into the Cloud, and then handed off to Jen, where she walked us through how we can actually respond to that We can call out to the Cloud Vision API, run our magical algorithm to find out how much money a receipt cost, and then wrote that to the database Kat has helped us through how we can aggregate that information in various interesting ways to get our team cost, as well as our individual user cost that’s then written back through Firestore, and synchronized out to all of our clients It was a really, really cool project, but we’re not done yet We want to share this all with you If you want to go to and start uploading your receipts, you can just share that cost, and then we can aggregate across all of I/O to see how much we have all spent It’s a nice, fun way to not feel so bad about what you’re expensing, in relation to what all of us are expensing The code will also be up on GitHub at a later date Don’t worry if you didn’t quite catch everything that we’re doing You’ll be able to build your own expense trackers in the near future Thank you all very much I hope you all enjoyed the first day of I/O. Go out there, put on sunscreen Don’t get sunburn like me, and have to get some aloe There are a couple of other talks The What’s New in Firebase talk, Wednesday, at 10:30 A deeper dive on Firestore data modeling, and then also architecting mobile web apps if you’re really interested in the web Thank you all very much Enjoy the rest of your day [MUSIC PLAYING]