Overview

Firefeed is an application modeled after Twitter.
You can post 141 character messages, follow other users, and view messages as soon as they are posted.

So what's special about Firefeed?

It's an entirely client side app.

No server logic or servers are needed.
Any place that can host static content, for example
Github Pages or
Site44, will be able to run this app
by simply serving the requisite HTML, CSS and JS files. You can easily serve it
from a CDN, since there's no dynamic content or caching to worry about!

The core logic is less than 500 lines of code.

Excluding the user interface, all the application logic is in two files:
firefeed.js
and
rules.json.
The former contains a thin abstraction over the data stored in Firebase, and the latter is
a small JSON file containing all the security rules that Firefeed
must adhere by.

Firefeed is made possible by Firebase,
a scalable real-time backend with a simple JS API. All Firefeed messages are stored there, and the API is used to update every user's feed with the latest messages in realtime. Firebase also has a rules language, that allows the developer to enforce certain security requirements.

Sound interesting? Let's delve into the details. We'll assume you know a little
about Firebase and how data is stored in it. Be sure to read through
the basic documentation for Firebase
if you haven't already, or take the quick 5-minute tutorial!

Authentication

The very first step the user must perform in order to post a message
on the site is to login. We use Facebook
to authenticate the user, which looks something like this:

We first obtain a Facebook OAuth token for the current user after
they log in using the Facebook SDK, and then exchange it for
a token from Firebase. When we
configured
our Firebase, we entered our Facebook application ID and
secret. Thus, Firebase can be assured that the token could only have
been produced by Firefeed, the app that the user agreed to log into.

Firebase can take care of this whole process for us, so we'll
simply import the authentication library and make a couple of function
calls:

That's it! We now have a Firebase token for the user
who just logged in and authenticated our Firebase reference with it.

Security

Now we’ve implemented authentication, we'll specify some rules about how the data must be handled by Firebase. This is how we ensure our client-only app is secure.

Before we dive into the rules themselves, it's important to note that
the rules are tightly coupled to the data structure of your app.
That's why we're doing the rules first, before we've decided
how to structure our data. Since all the rules are in one place,
it's much easier to think and reason about your data!

Characteristics

Let's list out the basic characteristics that we want Firefeed to have (we'll call
every message in our system a spark):

All sparks are readable by anyone, even users who
aren't logged in. Sparks cannot be posted without logging in.

All sparks are non-mutable, so they cannot be modified, even
by the user that created them once they've been posted.

Every user has their own list of sparks posted, and nobody should
be able to add to this list but the user.

Every user has their own feed, which is a combined list of all
sparks posted by them, and by all the users that they are following.
No other sparks must appear in this feed.

Every spark post should be attributed to the currently logged in
user, and users should not be able to spoof one another.

Let's translate these requirements into a set of rules! We'll have
three top-level keys:

people: A list of registered users.

users: A dictionary containing a key for
every user which will contain their personal data (spark list
and feed).

sparks: A global list of sparks posted on
the site along with their metadata.

The Rules

Let's start off with top-level rules. Anyone can read
everything, nobody can write to everything:

{
rules: {
".read": true,
".write": false
}
}

The people key contains the list of registered users,
and we must ensure that a user should only be able to add their
own entry. We'll also let them modify the entry if they wish,
because we'll store some metadata about the user (such as the
profile picture URL) here. Remember the authentication token
we obtained earlier? The token also contains the user ID of the
user to whom the token was issued, which we will refer to in our
rules as auth.id:

{
people: {
$userid: {
".write": $userid == auth.id
}
}
}

The variable $userid refers to the key under people
for which the rule is evaluated. The write rule enforces that only
the currently logged in user can write their own entry. Cool!

Now, every user should be able to write into their own personal data
bucket. Let's have every user bucket contain three properties:
(1) a list of sparks posted by them, (2) a list of users they follow, and (3) a list of users who follow them. If you're wondering why we chose this particular structure, hold that thought, we'll come back to it in a minute!

So far so good. The write rule above will evaluate to true for
the currently logged in user, and they'll be able to write to
their data bucket. But wait, wasn't the top-level write rule
set to false? Yes, but Firebase will allow a write even if any
one of the write rules in the path from a node to the root of the
tree evaluates to true. Nifty!

We're not quite done yet, because even if the current
user follows someone, their messages won't appear.
We want to allow anyone who the user is following to be able
to write to the current user's feed:

Let's break these rules down. They consist of two expressions that must both evaluate to true for the write to be allowed. They also make use of the special root variable which points to the top-level of our Firebase data.

The first rule specifies that only the people followed by a particular
user are able to write to that user's feed. For example, if user A
follows user B, user B can write to user A's feed. The second rule
checks if the author of the spark
being written is the same as the user who's trying to do the write.
This means that user B can only write sparks authored by themselves into
user A's feed.

Now let's make sure that every user in the following
list is a valid user, and that anyone is able to add themselves to
the follower list:

We're almost there! We'll need to define the rules for the global
sparks list. We need to ensure is that every spark is formatted a
certain way, that sparks cannot be modified once posted, and that
only the author of a spark is able to add it to the global list:

Easy! We made use of two new variables: data refers
to the object currently present at that location, and newData
refers to the data that will be written if the rule succeeds.

Phew - we're done with the rules! The hardest part is behind us
now, when it's all put together, it makes a small 60 line
JSON file.
Now Firebase will ensure that no data will be written where it wasn't meant to.

Application Logic

Let's come back to why we decided on this data structure:
A users feed and a user’s sparks will just be a list of spark IDs. The actual spark data will be stored in a global spark list. When a new spark is posted, we’ll it in the global list, and then put its ID in the feed of every user that is following the author.

With Firebase queries and limits,
we can also limit the output to the latest
100 sparks so we don't fetch any more data than is needed,
further improving our performance.

Exercises for the reader

This is just the foundation for a Twitter-like system. There are many features one can
add to customize the experience. Here are some ideas to get you started!

Implement unfollowing a user.

Implement protected accounts. If a user opts into a protected account, their
sparks can only be viewed by people they approve.

Implement search based on #hashtags. Hint: Instead of searching for the
actual value through the global list of sparks, consider creating a new
bucket for every hashtag when it is first used, and then populating it with spark
references.

Implement retweets, favorites and @ messages to other users. (Hint: You
can put the @ messages in the correct place at creation time).

Implement direct messages. (Hint: Use a mailbox style system between users, and
setup your rules such that you can write to another user's mailbox but not read
from it).

We did it!

That was the story of how you can build a Twitter clone in
under 500 lines of well commented code with copious whitespace.

We're pretty excited about this novel
approach to securing client side apps. When combined with Firebase's
realtime data updates, the doors have been opened to a wide
range of innovative applications. We can't wait to see what you'll come
up with, and we're here to help!