Posted by Jason Mill, Flamelink
If you read this blog, chances are you’re a fan of Firebase and the power it gives developers to build incredible products. At Flamelink we’ve used it extensively to build websites, apps and other digital platforms for clients of our agency. Firebase helps take your app from proof of concept to production quickly and easily.
While Firebase backend services like Realtime Database and Cloud Firestore are a great experience for developers, we have struggled to hand over full control to some of our less-technical clients. We found they weren’t comfortable editing data directly, so we were inundated with requests to do small content updates.
We tried many different solutions to make it possible for our clients to update their content - from hard-coding content on the frontend, to syncing Firebase with Google Sheets, and everything in between. We even tried coaching our non-developer clients on how to update content in the Firebase console, but the console was just too powerful for their needs.
So we created Flamelink, a headless Content Management System (CMS) built specifically for Firebase which supports both Cloud Firestore and the Realtime Database. Flamelink allows developers to leverage the power of Firebase and the Google Cloud Platform while giving non-developers an intuitive interface to manage their content.
In this tutorial, we’ll show you how to connect Flamelink to your Firebase project, and quickly get started building an app with Angular, Firebase and Flamelink. As with any Firebase project, you’re not limited to JavaScript, you can use any of the languages that Firebase supports.
A special thanks to our friend Erik Slack for providing this repo of example code to help you get started. You can find him on GitHub, LinkedIn or Twitter.
For the purposes of this tutorial, we’ll be creating a Bedtime Stories app, using long and short story examples to help illustrate how Flamelink can help you manage your content. You can clone the repo by running:
git clone https://github.com/erik-slack/Angular-Flamelink-Firebase.git
In this article, we’ll be covering these steps:
Let’s dive in!
If you haven’t already, go to the Firebase console and sign in. Once you’ve done that, you’ll want to select “Add project” and enter a name for your app.
Once you’ve done this, you’ll be asked a few more questions. For the purposes of this tutorial the choices you make won't matter. After you answer those questions your new Firebase project will be ready!
In this step, we’ll be configuring Firebase Authentication and Security Rules. This is to ensure that you can create, read and update content or files via the Flamelink interface.
First, in the Authentication section of the Firebase console, enable your prefered sign-in methods. You’ll need to configure this to log into your Firebase project via the Flamelink interface.
Then, confirm your authorized domains. Be sure to add the app.flamelink.io domain. This step allows your project’s Firebase Authentication to be used directly by Flamelink.
Next we’ll need to configure the Security Rules. In this tutorial, we’ll be using Cloud Firestore, so look for the Firestore section in the side-panel, create a database if necessary, and then select the Rules tab.
We'll start with the following rules. Note that these rules will allow any user to read / write any content in your database so you’ll need to write more secure rules before launching. These rules are not secure and you should not use them in a real application. For information on how to write more secure rules, check out the documentation.
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth.uid != null; } } }
We’ll also need to configure the rules for Cloud Storage for Firebase. Go to the Storage section in the left panel and select the Rules tab once again.
Copy the below code snippet into your Storage Rules:
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.auth != null; } match /flamelink/{allPaths=**} { allow read; allow write: if request.auth != null; } } }
Again, these rules are the bare-minimum needed to get a demo working and they are not recommended for production. You must write secure rules before launching your app.
At this point, you'll need to link your Firebase project to a new Flamelink project. You can follow this tutorial video to help you get started.
First, create some schemas. A schema is a form view of your data to easily create your content in a repeatable, structured way. For the bedtime stories example project we used two schemas: shortStory and longStory.
shortStory
longStory
Then, using these schemas, let’s create 5 different Short Stories and 3 Long Stories with whatever filler content you’d like.
Remember, you don’t need to spend too much time on this, just have enough to start building your application. This content is meant to be managed by non-developer content editors and clients.
The sample app in this tutorial was built using nrwl/nx, so there’s a lot going on, but we will only be working with these files.
You’re welcome to explore / modify any other files in the project, get creative!
From the root of the project directory, run either npm install (or yarn install) to install the dependencies.
npm install
yarn install
Because this is an Angular site, we'll use the AngularFire library to quickly and easily add authentication. Let’s create FirebaseService in shared/feature/core. This lets you keep track of auth and allows you to easily sign out. We’ll add FirebaseUI to your login component markup to provide a quick and easy authentication dialog for your users.
FirebaseService
shared/feature/core
<firebase-ui (signInSuccessWithAuthResult)="successCallback($event)" (signInFailure)="errorCallback($event)"> </firebase-ui>
When a use logs in successfully, we want to have it route the logged in user to home.module so add this to your ngOnInit function in the home.component.ts:
home.module
ngOnInit
home.component.ts
this.firebaseService.authState$.subscribe( this.firebaseAuthChangeListener.bind(this));
Next we'll integrate the Flamelink SDK, which is powered by the Firebase SDK, to load content into our app. To do this, create a new service called FlamelinkService in shared/feature/core. This service will be used to subscribe and fetch data when a user is logged in.
Add this to your ngOnInit function in the home.html:
home.html
private firebaseAuthChangeListener(response): void { if (response) { this.loggedIn = true; this.shortStoriesRef = this.flameLinkService.getApp().content .subscribe({ schemaKey: 'shortStory', fields: ['name', 'author', 'text'], callback: (error, data) => { if (error) { return console.error(error); } console.log('data', data); this.shortStories = Object.values(data); this.shortStoriesFacade.loadShortStories(data); } }); this.longStoriesRef = this.flameLinkService.getApp().content .subscribe({ schemaKey: 'longStory', fields: ['name', 'author', 'sections'], callback: (error, data) => { if (error) { return console.error(error); } console.log('data', data); this.longStories = Object.values(data); // this.longStoriesFacade.loadLongStories(data); } }); } else { this.loggedIn = false; if (this.shortStoriesRef) { this.shortStoriesRef(); // unsubscribe from short stories listener } if (this.longStoriesRef) { this.longStoriesRef(); // unsubscribe from long stories listener } } }
Add this to your ngOnInit function in the home.component.ts:
<li *ngFor="let shortStory of shortStories"> <short-story [shortStory]="shortStory" (click)="storyClicked('short', shortStory)"> </short-story> </li>
Remember to update your security rules before launching. This tutorial used insecure security rules to get you up and running as quickly as possible, you will need to update them before releasing your app.
Here are two Flamelink-specific tutorials for writing Security Rules:
We built Flamelink to enhance the Firebase developer experience. Firebase is such a versatile platform, it’s been mind-blowing for us to see how developers are using Firebase and Flamelink to manage content in a diverse range of use cases. We’ve seen Android and iOS apps, PWAs, VR and AR experiences, IoT platforms, websites, blogs, e-commerce/retail platforms, and AI and ML applications that all leverage Firebase and the Google Cloud Platform. So head over to Flamelink.io, sign up for a 14-day free trial and create your first project.
Hey, there Firebase developers. Did you hear the exciting news? Last month at Google I/O, we announced support for collection group queries in Cloud Firestore! Let's dig into this new feature a little more, and see if we answer some of your burning questions…
Q: So, what are collection group queries and why should I care?
In Cloud Firestore, your data is divided up into documents and collections. Documents often point to subcollections that contain other documents, like in this example, where each restaurant document contains a subcollection with all the reviews of that restaurant.
In the past, you could query for documents within a single collection. But querying for documents across multiple collections wasn't possible. So, for instance, I could search for all reviews for Tony's Tacos, sorted by score, because those are in a single subcollection.
But if I wanted to find reviews for all restaurants where I was the author, that wasn't possible before because that query would span multiple reviews collections.
reviews
But with collection group queries, you're now able to query for documents across a collection group; that is, several collections that all have the same name. So I can now search for all the reviews I've written, even if they're in different collections.
Q: Great! So how do I use them?
The most important step in using a collection group query is enabling the index that allows you to run a query in the first place. Continuing our example, if we want to find all reviews that a particular person has written, we would tell Cloud Firestore, "Go index every author field in every single reviews collection as if it were one giant collection."
author
You can do this manually by going to the Firebase Console, selecting the "Index" tab for Cloud Firestore, going to the "Single Field" section, clicking the "Add exemption" button, specifying you want to create an exemption for the "reviews" collection with the "author" field and a "collection group" scope, and then enabling ascending and/or descending indexes.
But that's a lot of steps, and I tend to be pretty lazy. So, instead, I like enabling collection group indexes the same way I enable composite indexes. First, I'll write the code for the collection group query I want to use and attempt to run it. For example, here's some sample code I might write to search for all reviews where I'm the author.
var myUserId = firebase.auth().currentUser.uid; var myReviews = firebase.firestore().collectionGroup('reviews') .where('author', '==', myUserId); myReviews.get().then(function (querySnapshot) { // Do something with these reviews! })
Notice that I'm specifying a collectionGroup() for my query instead of a collection or document.
collectionGroup()
When I run this code, the client SDK will give me an error message, because the collection group index hasn't been created yet. But along with this error message is a URL I can follow to fix it.
Following that URL will take me directly to the console, with my collection group index ready to be created.
Once that index has been created, I can go ahead and re-run my query, and it will find all reviews where I'm the author.
If I wanted to search by another field (like rating), I would need to create a separate index with the rating field path instead of the author field.
rating
Q: Any gotchas I need to watch out for?
Why, yes! There are three things you should watch out for.
First, remember that collection group queries search across all collections with the same name (e.g., `reviews`), no matter where they appear in my database. If, for instance, I decided to expand into the food delivery service and let users write reviews for my couriers, then suddenly my collection group query would return reviews both for restaurants and for couriers in the same query.
This is (probably) not what I want, so the best thing to do would be to make sure that collections have different names if they contain different objects. For example, I would probably want to rename my courier review collections something like courier_reviews.
courier_reviews.
If it's too late to do that, the second best thing would be to add something like an isCourier Boolean field to each document and then limit your queries based on that.
isCourier
Second, you need to set up special security rules to support queries. You might think in my example that if I had a security rule like this:
I would be able to run this collection group query. After all, all of my review documents would fall under this rule, right? So why does this fail?
Well if you've seen our video on Cloud Firestore security rules, you would know that when it comes to querying multiple documents, Cloud Firestore needs to prove that a query would be allowed by the security rules without actually examining the underlying data in your database.
And the issue with my collection group query is that there's no guarantee it will only return documents in the restaurants → reviews collection. Remember, I could just as easily have a couriers → reviews collection, or a restaurant → dishes → reviews collection. Cloud Firestore has no way of knowing unless it examines the results of the data set.
So the better way to do this is to declare that any path that ends with "reviews" can be readable based on whatever security rules I want to implement. Something like this:
Note that this solution requires using version 2 of the security rules, which changes the way recursive wildcards work.
Third, keep in mind that these collection group indexes are counted against the 200 index exemptions limit per database. So before you start creating collection group indexes willy-nilly, take a moment and ask yourself what queries you really want to run, and just create indexes for those. You can always add more later.
Q: Can I do collection group queries for multiple fields?
Yes. If you're doing equality searches across multiple fields, just make sure you have an index created for each field with a collection group scope.
If you're combining an equality clause with a greater-than-or-less-than clause, you'll need to create a composite index with a collection group scope. Again, I find it's best to just try to run the query in the code and follow the link to generate the index. For instance, trying to run a collection group query for all reviews that I wrote with a rating of 4 or higher gave me a URL that opened this dialog box.
Q: It still seems like I could do all of this in a top-level collection. How are collection group queries better?
So this question is based on the idea that one alternative to creating collection group queries is to not store data hierarchically at all, and just store documents in a separate top level collection.
For instance, I could simply keep my restaurants and my reviews as two different top-level collections, instead of storing them hierarchically.
With this setup, I can still search for all reviews belonging to a particular restaurant…
As well as all reviews belonging to a particular author…
And you'll notice that with the separate top level collection, I no longer need to use one of my 200 custom indexes to create this query.
So, why go with the subcollection setup? Are collection group queries needed at all? Well, one big advantage to putting documents into subcollections is that if I expect that I'll want to order restaurant reviews by rating, or publish date, or most upvotes, I can do that within a reviews subcollection without needing a composite index. In the larger top level collection, I'd need to create a separate composite index for each one of those, and I also have a limit of 200 composite indexes.
Also, from a security rules standpoint, it's fairly common to restrict child documents based on some data that exists in their parent, and that's significantly easier to do when you have data set up in subcollections.
So when should you store things in a separate top level collection vs. using subcollections? If you think you have a situation where you're mostly going to be querying documents based on a common "parent" and only occasionally want to perform queries across all collections, go with a subcollection setup and enable collection group queries when appropriate. On the other hand, if it seems like no matter how you divide up your documents, the majority of your queries are going to require a collection group query, maybe keep them as a top level collection.
But if that's too hard to figure out, I would say that you should pick the solution that makes sense to you intuitively when you first think about your data. That tends to be the correct answer most of the time.
Hope that helps you get more comfortable with collection group queries! As always, if you have questions, feel free to check out our documentation, or post questions on Stack Overflow.
MightySignal, a mobile intelligence startup based out of San Francisco, just published a new report examining the fastest growing Android SDKs of 2017. This fascinating report sheds light on which app development tools are taking off and what trends they signal for the year ahead. We're humbled and excited to see that 8 out of the top 20 fastest growing SDKs are part of Firebase!
This positive reception from the community validates and fuels our commitment to helping developers succeed. It also motivates us to continue making Firebase even better. Over the past year, we've made numerous improvements to our SDKs, including the ones highlighted in MightySignal's report.
Source: MightySignal's 2017 report on the fastest growing SDKs
For example, Firebase Realtime Database is number three on MightySignal's list, and it continues to be one of our most used and trusted products. We understand how important storing and syncing data is for your mobile business, and to further help you with this, we introduced another database product this year. If you're a Realtime Database customer, we think you'll love Cloud Firestore, our latest realtime, scalable NoSQL database that we built in collaboration with the Google Cloud Platform team. It allows you to sync and store data like Realtime Database, while also addressing its key limitations like data structuring, querying, and scaling. Cloud Firestore is available in beta today!
Another notable mention is Firebase Remote Config. Remote Config gives you the power to customize your app's interface and behavior for different audiences so you can deliver personalized app experiences without requiring users to update their apps. Now, Remote Config can be used with Firebase Predictions' dynamic user groups. This means you can change the look and feel of your app for users based on their predicted behavior (such as churn or in-app purchase). Wondering how this works? Learn how Halfbrick Studios grew their 7-day retention rate from 25% to 30% by combining Predictions with Remote Config.
And that's not all that's new with Remote Config! In the past, Remote Config allowed you to perform simple A/B testing, but now, we've gone ahead and added an entirely new experiment layer in Firebase that works wonderfully with Remote Config so you can set up, run, and measure sophisticated A/B tests.
We were also delighted to see that Firebase Auth and Firebase Crash Reporting are experiencing high growth as well, according to MightySignal's findings. After welcoming the Fabric team to Firebase, we worked together to add new features to Auth (such as phone number authentication), which we unveiled in June. More recently, we launched a beta version of Firebase Crashlytics, a powerful realtime crash reporting tool that will help you track, prioritize, and fix issues that erode app stability. Firebase Crashlytics is now our primary crash reporter. If you want to learn more about how app stability can lead to growth in user engagement and retention, check out how Doodle used Crashlytics to grow user engagement by 42%.
MightySignal's data on the fastest growing SDKs is available here. We're very thankful to be part of the developer community and committed to helping you build better apps and grow your business. Stay tuned for more product updates next year and, in the meantime, happy building!
Today we're excited to launch Cloud Firestore, a fully-managed NoSQL document database for mobile and web app development. It's designed to easily store and sync app data at global scale, and it's now available in beta.
Key features of Cloud Firestore include:
And of course, we've aimed for the simplicity and ease-of-use that is always top priority for Firebase, while still making sure that Cloud Firestore can scale to power even the largest apps.
Managing app data is still hard; you have to scale servers, handle intermittent connectivity, and deliver data with low latency.
We've optimized Cloud Firestore for app development, so you can focus on delivering value to your users and shipping better apps, faster. Cloud Firestore:
As you may have guessed from the name, Cloud Firestore was built in close collaboration with the Google Cloud Platform team.
This means it's a fully managed product, built from the ground up to automatically scale. Cloud Firestore is a multi-region replicated database that ensures once data is committed, it's durable even in the face of unexpected disasters. Not only that, but despite being a distributed database, it's also strongly consistent, removing tricky edge cases to make building apps easier regardless of scale.
It also means that delivering a great server-side experience for backend developers is a top priority. We're launching SDKs for Java, Go, Python, and Node.js today, with more languages coming in the future.
Over the last 3 years Firebase has grown to become Google's app development platform; it now has 16 products to build and grow your app. If you've used Firebase before, you know we already offer a database, the Firebase Realtime Database, which helps with some of the challenges listed above.
The Firebase Realtime Database, with its client SDKs and real-time capabilities, is all about making app development faster and easier. Since its launch, it has been adopted by hundred of thousands of developers, and as its adoption grew, so did usage patterns. Developers began using the Realtime Database for more complex data and to build bigger apps, pushing the limits of the JSON data model and the performance of the database at scale. Cloud Firestore is inspired by what developers love most about the Firebase Realtime Database while also addressing its key limitations like data structuring, querying, and scaling.
So, if you're a Firebase Realtime Database user today, we think you'll love Cloud Firestore. However, this does not mean that Cloud Firestore is a drop-in replacement for the Firebase Realtime Database. For some use cases, it may make sense to use the Realtime Database to optimize for cost and latency, and it's also easy to use both databases together. You can read a more in-depth comparison between the two databases here.
We're continuing development on both databases and they'll both be available in our console and documentation.
Cloud Firestore enters public beta starting today. If you're comfortable using a beta product you should give it a spin on your next project! Here are some of the companies and startups who are already building with Cloud Firestore:
Get started by visiting the database tab in your Firebase console. For more details, see the documentation, pricing, code samples, performance limitations during beta, and view our open source iOS and JavaScript SDKs on GitHub.
We can't wait to see what you build and hear what you think of Cloud Firestore!
One of my favorite parts about working with the Cloud Functions for Firebase team is helping developers move logic from their mobile apps to a fully managed backend hosted by Firebase. With just a few lines of JavaScript code, they're able to unify logic that automatically takes action on changes to the contents of their Realtime Database. It's really fun to see what people build!
When Cloud Functions was first announced at Cloud Next 2017, there was just one trigger available for all types of changes to the database. This trigger is specified using the onWrite() callback, and it was the code author's responsibility to figure out what sort of change occurred. For example, imagine your app has a chat room, and you want to use Firebase Cloud Messaging to send a notification to users in that room when a new message arrives. To implement that, you might write some code that looks like this:
onWrite()
exports.sendNotification = functions.database .ref("/messages/{messageId}").onWrite(event => { const dsnap = event.data // a DeltaSnapshot that describes the write if (dsnap.exists() && !dsnap.previous.exists()) { // This is a new message, not a change or a delete. // Send notifications with FCM... } })
Note that you have to check the DeltaSnapshot from the event object to see if new data now exists at the location of the write, and also if there was no prior data at that location. Why is this necessary? Because onWrite() will trigger on all changes to data under the matched location, including new messages, updated messages, and deleted messages. However, this function is only interested in new messages, so it has to make sure the write is not an update or a delete. If there was an update or delete, this function would still get triggered and return immediately. This extra execution costs money, and we know you'd rather not be billed for a function that doesn't do useful work.
The good news is that these extra checks are no longer necessary with Cloud Functions database triggers! Starting with the firebase-functions module version 0.5.9, there are now three new types of database triggers you can write: onCreate(), onUpdate(), and onDelete(). These triggers are aware of the type of change that was made, and only run in response to the type of change you desire. So, now you can write the above function like this:
onCreate()
onUpdate()
onDelete()
exports.sendNotification = functions.database .ref("/messages/{messageId}").onCreate(event => { // Send notifications with FCM... })
Here, you don't have to worry about this function getting triggered again for any subsequent updates to the data at the same location. Not only is this easier to read, it costs you less to operate.
Note that onWrite() hasn't gone away. You can still keep using it with your functions for as long as you like.
If you've previously used onWrite() to add or change data at a location, you're aware that the changes you made to that data during onWrite() would trigger a second invocation of onWrite() (because all writes count as writes, am I right?).
Consider this function that updates the lastUpdated property of a message after it's written:
exports.lastUpdate = functions.database .ref("/messages/{messageId}").onWrite(event => { const msg = event.data.val() msg.lastUpdated = new Date().getTime() return event.data.adminRef.set(msg) })
This seems OK at first, but there's something very important missing. Since this function writes back to the same location where it was triggered, it will effectively trigger another call to onWrite(). You can see here that this will cause an infinite loop of writes, so it needs some way to bail out of the second invocation.
It turns out that onUpdate() function implementations share this concern. One solution in this specific case could be to check the existing value of lastUpdated and bail out if it's not more than 30 seconds older than the current date. So, if we want to rewrite this function using onUpdate(), it could look like this:
exports.lastUpdate = functions.database .ref("/messages/{messageId}").onUpdate(event => { const msg = event.data.val() const now = new Date().getTime() if (msg.lastUpdated > now - (30*1000)) { return } msg.lastUpdated = now return event.data.adminRef.set(msg) })
This function now defends against infinite loops with a little extra logic.
To start using these new triggers, be sure to update your firebase-functions module to 0.5.9. And for more information about these updates, check out the documentation right here.
Last April, we announced the general availability of the Firebase Admin SDK for Python. The initial release of this SDK supported two important features related to Firebase Authentication: minting custom tokens, and verifying ID tokens. Now, we are excited to announce that database support is available in the Firebase Admin SDK for Python starting from version 2.1.0.
Due to the way it's implemented, there are several notable differences between this API and the database APIs found in our other Admin SDKs (Node.js and Java). The most prominent of these differences is the lack of support for realtime event listeners. The Python Admin SDK currently does not provide a way to add event listeners to a database reference in order to receive realtime update notifications. Instead, all data retrieval operations are provided as blocking methods. However, despite these differences there's a lot that can be achieved using this API.
The database module of the Python Admin SDK facilitates both basic data manipulation operations and advanced queries. To begin interacting with the database from a Python environment, initialize the SDK with the Realtime Database URL:
import firebase_admin from firebase_admin import credentials cred = credentials.Cert('path/to/serviceKey.json') firebase_admin.initialize_app(cred, { 'databaseURL' : 'https://my-db.firebaseio.com' })
Then obtain a database reference from the db module of the SDK. Database references expose common database operations as Python methods (get(), set(), push(), update() and delete()):
db
get(), set(), push(), update()
delete()
from firebase_admin import db root = db.reference() # Add a new user under /users. new_user = root.child('users').push({ 'name' : 'Mary Anning', 'since' : 1700 }) # Update a child attribute of the new user. new_user.update({'since' : 1799}) # Obtain a new reference to the user, and retrieve child data. # Result will be made available as a Python dict. mary = db.reference('users/{0}'.format(new_user.key)).get() print 'Name:', mary['name'] print 'Since:', mary['since']
In the Firebase Realtime Database, all data values are stored as JSON. Note how the Python Admin SDK seamlessly converts between JSON and Python's native data types.
To execute an advanced query, call one of the order_by_* methods available on the database reference. This returns a query object, which can be used to specify additional parameters. You can use this API to execute limit queries and range queries against your data, and retrieve sorted results.
order_by_*
from firebase_admin import db dinos = db.reference('dinosaurs') # Retrieve the five tallest dinosaurs in the database sorted by height. # 'result' will be a sorted data structure (list or OrderedDict). result = dinos.order_by_child('height').limit_to_last(5).get() # Retrieve the 5 shortest dinosaurs that are taller than 2m. result = dinos.order_by_child('height').start_at(2).limit_to_first(5).get() # Retrieve the score entries whose values are between 50 and 60. result = db.reference('scores').order_by_value() \ .start_at(50).end_at(60).get()
Take a look at the Admin SDK documentation for more information about this new API. Also check out our Github repo, and help us further improve the Admin SDK by reporting issues and contributing patches. In fact, it was your continuing feedback that motivated us to build and release this API in such a short period. Happy coding with Firebase!