Firebase Isn’t Just a Database—It’s a Back End for Front-End Developers
Firebase started as a side effect. In 2013, Andrew Lee and James Tamplin were running Envolve, a chat widget that developers dropped onto pages with a single script tag. As usage grew, they noticed something odd: some developers were hiding the widget entirely and using its message channel to sync game scores, app settings, and to-dos between users. The chat data was incidental; the real-time synchronization was the point.
That observation became the foundation of Firebase. The original product was a real-time cloud database. Today, it’s a platform of 19 products that together can replace a traditional back end—or stand in as individual services. The common thread: infrastructure you don’t have to manage yourself.
Nineteen Products, One Platform
Firebase’s services cover the usual back-end responsibilities, plus a layer of monitoring and engagement tools. A sampling:
- Hosting: Deploy a site preview for every GitHub pull request.
- Firestore: A real-time, offline-capable document database.
- Auth: User authentication with multiple providers.
- Storage: Stores user-generated files like images and video.
- Cloud Functions: Event-driven server code.
- Extensions: Pre-built functions (e.g., Stripe payments, text translation) set up via UI.
- Google Analytics: Segments and audiences for user activity.
- Remote Config: A key-value store with dynamic conditions, useful for feature gating.
- Performance Monitoring: Load metrics and custom traces.
- Cloud Messaging: Cross-platform push notifications.
Each of these works independently. A Firebase project is only a container; it doesn’t force a particular stack.
Standing Up a Project
Before coding, you need a Firebase project. From the Firebase Console, create a new project (an optional Google Analytics step can be skipped) and register a web app. The console returns a config object with project identifiers.

let firebaseConfig = {
apiKey: "your-key",
authDomain: "your-domain.firebaseapp.com",
projectId: "your-projectId",
storageBucket: "your-projectId.appspot.com",
messagingSenderId: "your-senderId",
appId: "your-appId",
measurementId: "your-measurementId"
};
Those config values are safe to ship in front-end code. Knowing a database’s address doesn’t grant access; security is enforced elsewhere. Load the Firebase libraries (via CDN, Webpack, Rollup, etc.) and you’re ready to connect.
// This pen adds Firebase via the "Add External Scripts" option in codepen
// https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js
// https://www.gstatic.com/firebasejs/8.2.10/firebase-auth.js
// Create a Project at the Firebase Console
// (console.firebase.google.com)
let firebaseConfig = {
apiKey: "your-key",
authDomain: "your-domain.firebaseapp.com",
projectId: "your-projectId",
storageBucket: "your-projectId.appspot.com",
messagingSenderId: "your-senderId",
appId: "your-appId",
measurementId: "your-measurementId"
};
// Create your Firebase app
let firebaseApp = firebase.initializeApp(firebaseConfig);
// The auth instance
console.log(firebaseApp.auth());
Two console steps remain. First, enable Auth providers. New projects start with all sign-in methods off—a default that keeps your backend closed. Under Authentication → Sign-in method, turn on Google (you’ll be asked for a support email) and Anonymous, near the bottom of the list.


Then enable Firestore. Create the database in “test mode,” meaning open to all requests. That default is just for development; we’ll write real security rules shortly.

Auth Guests and Google Sign-In
Firebase provides guest accounts through anonymous auth. Users require no input, yet they get a uid to associate with server-side data:
// Firebase-specific code
let firebaseConfig = { /* config */ };
let firebaseApp = firebase.initializeApp(firebaseConfig);
// End Firebase-specific code
let socialForm = document.querySelector('form.sign-in-social');
let guestForm = document.querySelector('form.sign-in-guest');
guestForm.addEventListener('submit', async submitEvent => {
submitEvent.preventDefault();
let formData = new FormData(guestForm);
let displayName = formData.get('name');
let photoURL = await getRandomPhotoURL();
// Firebase-specific code
let { user } = await firebaseApp.auth().signInAnonymously();
await user.updateProfile({ displayName, photoURL });
// End Firebase-specific code
});
That’s two calls: signInAnonymously() authenticates, and user.updateProfile() gives even guest accounts a display name or photo. Social providers work the same way, only the method changes:
socialForm.addEventListener('submit', submitEvent => {
submitEvent.preventDefault();
// Firebase-specific code
let provider = new firebase.auth.GoogleAuthProvider();
firebaseApp.auth().signInWithRedirect(provider);
// End Firebase-specific code
});
Providers like Google use a redirect flow: the method bounces the user to the provider’s sign-in page, then returns them to your app authenticated. You detect the result globally with an auth-state listener:
firebaseApp.auth().onAuthStateChanged(user => {
if(user != null) {
console.log(user.toJSON());
} else {
console.log("No user!");
}
});
onAuthStateChanged fires on page load and whenever a user logs in or out, making it a natural hook for routing. In demos, this is often a straightforward view swap:
<div class="container">
<div class="phone">
<!-- Phone contents replaced with template tags -->
</div>
</div>
firebaseApp.auth().onAuthStateChanged(user => {
if(user != null) {
// Show demo view
routeTo("demo", firebaseApp, user);
} else {
console.log("No user!");
// Show log in page
routeTo("signIn", firebaseApp);
}
});
Merging Guests Into Permanent Accounts
Anonymous users aren’t meant to stay anonymous. The typical path is to let a guest trigger a social sign-in, then merge the two identities with linkWithRedirect.
let convertForm = document.querySelector('form.convert');
convertForm.addEventListener("submit", submitEvent => {
submitEvent.preventDefault();
let provider = new firebase.auth.GoogleAuthProvider();
firebaseApp.auth().currentUser.linkWithRedirect(provider);
});
The crucial detail: after merging, the uid stays the same, so all data created during the guest session is preserved. Your auth listener doesn’t even need to change. Edge cases exist—if a user tries to link with a provider that already has its own account, it throws an error. That error contains the credentials needed to complete a manual merge:
async function checkForRedirect() {
let auth = firebaseApp.auth();
try {
let result = await auth.getRedirectResult();
}
catch (error) {
switch(error.code) {
case 'auth/credential-already-in-use': {
// You can check for the provider(s) in use
let providers = await auth.fetchProvidersForEmail(error.email);
// Then decide what strategy to take. A possible strategy is
// notifying the user and asking them to sign in as that account
}
}
}
}
Streaming Data to a Live View
The core trick for live views is pairing CSS conic-gradient pie charts with Firestore snapshots. The gradient handles the drawing; a style block holds segment percentages as custom properties.
.pie-chart {
background-image: conic-gradient(
purple 10%, /* 10% of the circumference */
magenta 0 20%, /* start at 0 go 20%, acts like 10% */
cyan 0 /* Fill the rest */
);
}
:root {
--pie-1-value: 10%;
--pie-2-value: 10%;
--pie-3-value: 80%;
--pie-1-computed: var(--pie-1-value);
--pie-2-computed: 0 calc(var(--pie-1-value) + var(--pie-2-value));
--pie-3-computed: 0 calc(var(--pie-2-value) + var(--pie-3-value));
}
background-image: conic-gradient(
purple var(--pie-1-computed),
magenta var(--pie-2-computed),
cyan 0
);
The last computed value always fills the circle’s remainder, though it’s still worth setting in JavaScript for consistency:
function setPieChartValue(percentage, index) {
let root = document.documentElement;
root.style.setProperty(`--pie-${index+1}-value`, `${percentage}%`);
}
let percentages = [25, 35, 60];
percentages.forEach(setPieChartValue);
Instead of a one-time .get(), you subscribe to a doc with .onSnapshot(). Every local or remote change pushes a live update to connected clients.
const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
fullPathDoc.onSnapshot(snap => {
const { items } = doc.data();
items.forEach(setPieChartValue);
});
NoSQL Data Modeling: Collections and Paths
Firestore is hierarchical. Documents live in collections, and documents can contain sub-collections. The way you structure paths can reduce or eliminate the need for queries.
A naive model might place all of a user’s data in one readable collection with a uid field:
{
uid: '1234',
items: [
{ label: "Food", value: 10 },
{ label: "Services", value: 24 },
{ label: "Rent", value: 30 },
{ label: "Oops", value: 38 }
]
}
Retrieving that data requires a query:
// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;
// Reference to collection stored at: '/expenses'
const expensesCol = firebaseApp.firestore('expenses');
// Query for the expenses belonging to uid == 1234
const userQuery = expensesCol.where('uid', '==', currentUser.uid);
const snapshot = await userQuery.get();
But data paths resemble URLs. A hierarchical design embeds the owner into the path itself:
/users/:uid/expenses/month-year
With this shape, fetching a specific user’s month is a precise read:
// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;
// Reference to collection stored at: '/users'
const usersCol = firebaseApp.firestore().collection('users');
// Reference to document stored at: '/users/1234';
const userDoc = usersCol.doc(currentUser.uid);
// Reference to sub-collection stored at: '/users/1234/expenses'
const userExpensesCol = userDoc.collection('expenses');
// Reference to document stored at: '/users/1234/expenses/3-2021';
const marchDoc = userExpensesCol.doc('3-2021');
// Alternatively, you could express a full path:
const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
This direct-address style doesn’t fit every access pattern, but it shines when your queries are predictable. It also aligns well with how security rules are cast. Deep dives on modeling strategy are available in video series like Todd Kerpelman’s Firestore series.
With data in place, the streaming updates can drive chart values:
const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
fullPathDoc.onSnapshot(snap => {
const { items } = doc.data();
items.forEach((item, index) => {
console.log(item);
});
});
let db = firebaseApp.firestore();
let { uid } = firebaseApp.auth().currentUser;
let marchDoc = db.doc(`users/${uid}/expenses/3-2021`);
marchDoc.onSnapshot(snap => {
let { factors } = snap.data();
factors.forEach((factor, index) => {
root.style.setProperty(`--pie-${index+1}__value`, `${factor.value}%`);
});
});
Edits in the console arrive as movement in the pie chart almost instantly.

Securing Firestore with Rules
A client directly hitting your database shouldn’t be able to rewrite everything. Firebase handles access control with Security Rules—a language that Firebase evaluates server-side on each read, write, and delete request.
Rules resemble path-based routing. A match pattern identifies documents, and an allow statement specifies conditions.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// When a request comes in for a "user/:userId"
// let's allow the read or write (not very secure)
// Don't copy this in your code plz!
match /users/{userId} {
allow read, write: if userId == "david";
}
}
}
Here, {userId} is a route parameter. Matching rules are entered only if the logic passes. A global recursive wildcard—/{document=**}—combined with a rule that always evaluates true opens the entire database, and it would override all more-specific matches. That’s a suitable “test mode,” not a production design.
For the users collection, the desired rule is simpler: only the authenticated user touches their data. Known request objects make this concise.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{documents=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
The path and request are compared for equality of userId and the authenticated request.auth.uid. The syntax {documents=**} is a recursive wildcard, confidently applying the same logic to any nested sub-collections (e.g., expenses beneath a user). If no allow matches a path, that path is implicitly denied—an explicit, safe default.
Sharing data with another anonymous user remains disallowed, as a non-matching request demonstrates:
// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;
// The authenticated user owns this sub-collection
const ownedDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
// The authenticated user DOES NOT own this sub-collection
const notOwnedDoc = firebaseApp.firestore().doc('/users/abcxyz/expenses/3-2021');
try {
const ownedSnapshot = await ownedDoc.get();
const notOwnedSnapshot = await notOwnedDoc.get();
} catch (error) {
// This will result in an error because the `notOwnedDoc` request will fail the security rule
}
Be wary of overlapping matches. They don’t follow CSS’s “last rule wins” precedence. If any rule permits an operation, it proceeds:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Ahh!!! Never do this in a production app!!!
// This will negate any rule you write below!!
// Don't copy and paste this into your rules!!
match /{document=**} {
allow read, write: if true;
}
match /users/{userId}/{documents=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Because of that, the global recursive wildcard can silently open properties meant to be protected.
Rules are best treated as source code. New projects often edit them inside the Firestore console’s Rules tab, where it’s easy to prototype tests. Teams should look to the Firebase CLI to version-control rules and run them through tests locally.

Security rules perform a crucial function: they mitigate front-end configuration exposure. Your public config keys are not credentials; they’re equivalent to a domain name. Rules translate that gap into a principled barrier—an API for running pre-checks on data.
Where to Go from Here
Signing in users, merging guest identities, modeling hierarchical data, streaming snapshots into real-time UI, and wrapping it all in an explicit security layer repeats many app patterns with surprisingly little code.
Further steps aren’t hard to find. For local development and CI consistency, you can run the full stack locally with the Emulator Suite. Library integrations for common frameworks are ready-made, including AngularFire, ReactFire, VueFire, and RxFire. Broad engineering discussions remain active in the Firebase community and Google-run codelabs.



