March 27, 202612 min read

Firebase: From Zero to Deployed App (Authentication, Database, and Hosting)

Learn Firebase from project setup to deployment. Firestore, Firebase Auth, security rules, hosting, Cloud Functions, and a real-time notes app.

firebase google backend authentication tutorial
Ad 336x280

Firebase is Google's application platform, and it has a specific superpower: it lets frontend developers build full-stack apps without writing a traditional backend. No Express server, no API endpoints, no database management. You talk directly to Firebase services from your client-side code, and security rules protect everything.

Is that the right architecture for every app? No. But for real-time apps, MVPs, prototypes, and projects where you want to ship fast, it's hard to beat. Millions of apps run on Firebase, from hobby projects to apps with millions of users.

By the end of this tutorial, you'll build a real-time collaborative notes app with authentication, a NoSQL database, and deploy it to Firebase Hosting.

Project Setup

Firebase Console

  1. Go to console.firebase.google.com
  2. Click "Create a project" (or "Add project")
  3. Name your project, optionally enable Google Analytics
  4. Wait for provisioning (30 seconds)

Local Setup

# Install Firebase CLI
npm install -g firebase-tools

# Login
firebase login

# Initialize in your project directory
firebase init

The init wizard asks which services you want. Select:


  • Firestore (database)

  • Authentication

  • Hosting

  • Functions (optional, for server-side logic)


Add Firebase to Your App

npm install firebase
// lib/firebase.js
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
apiKey: "AIzaSy...",
authDomain: "your-project.firebaseapp.com",
projectId: "your-project-id",
storageBucket: "your-project.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const auth = getAuth(app);

Get these values from Project Settings > General > Your apps > Web app in the Firebase Console. These are safe to include in client-side code -- security comes from Firestore rules, not from hiding the config.

Firestore Data Model

Firestore is a NoSQL document database. Think of it like JSON trees:

users (collection)
  ├── user123 (document)
  │   ├── name: "Alice"
  │   ├── email: "alice@example.com"
  │   └── createdAt: timestamp
  │
  └── user456 (document)
      ├── name: "Bob"
      └── email: "bob@example.com"

notes (collection)
├── note789 (document)
│ ├── title: "Shopping List"
│ ├── content: "Milk, eggs, bread"
│ ├── userId: "user123"
│ ├── createdAt: timestamp
│ └── tags: ["personal", "shopping"]

└── noteABC (document)
├── title: "Meeting Notes"
├── content: "Discussed Q2 plans..."
├── userId: "user123"
└── createdAt: timestamp

Key concepts:


  • Collections contain documents. They're like tables.

  • Documents contain fields. They're like rows, but each document can have different fields.

  • Subcollections -- documents can contain nested collections. This is powerful for modeling hierarchical data.

  • Document IDs can be auto-generated or you can set them explicitly.


CRUD Operations

import {
  collection, doc, addDoc, getDoc, getDocs, updateDoc,
  deleteDoc, query, where, orderBy, limit, serverTimestamp,
  onSnapshot
} from 'firebase/firestore';
import { db } from './lib/firebase';

// CREATE -- auto-generated ID
async function createNote(userId, title, content) {
const docRef = await addDoc(collection(db, 'notes'), {
title,
content,
userId,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
});
return docRef.id;
}

// CREATE -- with specific ID
import { setDoc } from 'firebase/firestore';

async function createUserProfile(userId, name, email) {
await setDoc(doc(db, 'users', userId), {
name,
email,
createdAt: serverTimestamp()
});
}

// READ -- single document
async function getNote(noteId) {
const docSnap = await getDoc(doc(db, 'notes', noteId));

if (docSnap.exists()) {
return { id: docSnap.id, ...docSnap.data() };
}
return null;
}

// READ -- multiple documents with query
async function getUserNotes(userId) {
const q = query(
collection(db, 'notes'),
where('userId', '==', userId),
orderBy('createdAt', 'desc'),
limit(50)
);

const snapshot = await getDocs(q);
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
}

// UPDATE
async function updateNote(noteId, updates) {
await updateDoc(doc(db, 'notes', noteId), {
...updates,
updatedAt: serverTimestamp()
});
}

// DELETE
async function deleteNote(noteId) {
await deleteDoc(doc(db, 'notes', noteId));
}

serverTimestamp() is important. It uses the server's clock, not the client's, which avoids inconsistencies from users with wrong system clocks.

Real-Time Listeners

Firestore's real-time listeners are its standout feature. Instead of polling for changes, you subscribe and get instant updates:

// Listen to a single document
function subscribeToNote(noteId, callback) {
  return onSnapshot(doc(db, 'notes', noteId), (docSnap) => {
    if (docSnap.exists()) {
      callback({ id: docSnap.id, ...docSnap.data() });
    }
  });
}

// Listen to a query (collection of documents)
function subscribeToUserNotes(userId, callback) {
const q = query(
collection(db, 'notes'),
where('userId', '==', userId),
orderBy('updatedAt', 'desc')
);

return onSnapshot(q, (snapshot) => {
const notes = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
callback(notes);

// You can also track individual changes
snapshot.docChanges().forEach(change => {
if (change.type === 'added') console.log('New note:', change.doc.data());
if (change.type === 'modified') console.log('Updated:', change.doc.data());
if (change.type === 'removed') console.log('Deleted:', change.doc.id);
});
});
}

// Usage in React
useEffect(() => {
if (!user) return;

const unsubscribe = subscribeToUserNotes(user.uid, (notes) => {
setNotes(notes);
});

return () => unsubscribe(); // Clean up on unmount
}, [user]);

The onSnapshot function returns an unsubscribe function. Always call it when the component unmounts or when you no longer need updates.

Firebase Authentication

Firebase Auth supports email/password, Google, GitHub, Apple, phone number, and more:

import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  signOut,
  onAuthStateChanged,
  sendPasswordResetEmail
} from 'firebase/auth';
import { auth } from './lib/firebase';

// Sign up with email/password
async function signUp(email, password) {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
return userCredential.user;
}

// Sign in with email/password
async function signIn(email, password) {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return userCredential.user;
}

// Sign in with Google
async function signInWithGoogle() {
const provider = new GoogleAuthProvider();
const userCredential = await signInWithPopup(auth, provider);
return userCredential.user;
}

// Sign out
async function logOut() {
await signOut(auth);
}

// Password reset
async function resetPassword(email) {
await sendPasswordResetEmail(auth, email);
}

// Listen for auth state changes
function onAuthChange(callback) {
return onAuthStateChanged(auth, (user) => {
callback(user);
});
}

Enable providers in the Firebase Console under Authentication > Sign-in method. For Google sign-in, you just flip a switch. For email/password, same thing. Easy.

For Google OAuth, you may need to add your domain to the authorized domains list under Authentication > Settings.

Security Rules

Security rules are what make the "client talks directly to database" architecture safe. They run on Firebase's servers and are enforced before any read or write:

// firestore.rules
rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {

// Users can only read/write their own profile
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}

// Notes rules
match /notes/{noteId} {
// Anyone authenticated can read notes they own
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;

// Users can create notes (must set their own userId)
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.title is string
&& request.resource.data.title.size() > 0
&& request.resource.data.title.size() <= 200;

// Users can update their own notes (can't change userId)
allow update: if request.auth != null
&& resource.data.userId == request.auth.uid
&& request.resource.data.userId == request.auth.uid;

// Users can delete their own notes
allow delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}

// Default: deny everything else
match /{document=**} {
allow read, write: if false;
}
}
}

Key concepts:


  • request.auth -- The authenticated user making the request. null if not authenticated.

  • resource.data -- The existing document in the database (for read/update/delete).

  • request.resource.data -- The document the user is trying to write (for create/update).

  • Rules are deny-by-default. If no rule matches, the operation is denied.


Deploy rules:

firebase deploy --only firestore:rules

Test rules in the Firebase Console under Firestore > Rules > Rules Playground. This lets you simulate requests as different users.

Firebase Hosting

Firebase Hosting is a CDN-backed static hosting service. It's fast, free (with generous limits), and integrates seamlessly with your Firebase project:

# Build your app
npm run build

# Deploy to Firebase Hosting
firebase deploy --only hosting

Configuration in firebase.json:

{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "/.", "/node_modules/*"],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    "headers": [
      {
        "source": "*/.@(js|css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      }
    ]
  }
}

The rewrite rule sends all routes to index.html -- essential for single-page apps with client-side routing.

You get a free your-project.web.app domain, and you can add custom domains in the Firebase Console.

Cloud Functions Basics

For logic that shouldn't run on the client (sending emails, processing payments, admin operations), use Cloud Functions:

// functions/index.js
const { onDocumentCreated } = require('firebase-functions/v2/firestore');
const { onCall, HttpsError } = require('firebase-functions/v2/https');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');

initializeApp();
const db = getFirestore();

// Trigger: runs when a new note is created
exports.onNoteCreated = onDocumentCreated('notes/{noteId}', async (event) => {
const noteData = event.data.data();
const noteId = event.params.noteId;

// Example: update a counter on the user's profile
const userRef = db.doc(users/${noteData.userId});
const userDoc = await userRef.get();

if (userDoc.exists) {
const currentCount = userDoc.data().noteCount || 0;
await userRef.update({ noteCount: currentCount + 1 });
}
});

// Callable function: invoked directly from client
exports.deleteAllNotes = onCall(async (request) => {
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Must be signed in');
}

const userId = request.auth.uid;
const snapshot = await db.collection('notes')
.where('userId', '==', userId)
.get();

const batch = db.batch();
snapshot.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();

return { deleted: snapshot.size };
});

Call from the client:

import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();
const deleteAllNotes = httpsCallable(functions, 'deleteAllNotes');

const result = await deleteAllNotes();
console.log(Deleted ${result.data.deleted} notes);

Deploy:

firebase deploy --only functions

Building a Real-Time Notes App

Here's the complete React app:

// App.jsx
import { useState, useEffect } from 'react';
import { db, auth } from './lib/firebase';
import {
  collection, addDoc, updateDoc, deleteDoc, doc,
  query, where, orderBy, onSnapshot, serverTimestamp
} from 'firebase/firestore';
import {
  signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged
} from 'firebase/auth';

function App() {
const [user, setUser] = useState(null);
const [notes, setNotes] = useState([]);
const [selectedNote, setSelectedNote] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
return onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false);
});
}, []);

useEffect(() => {
if (!user) {
setNotes([]);
return;
}

const q = query(
collection(db, 'notes'),
where('userId', '==', user.uid),
orderBy('updatedAt', 'desc')
);

return onSnapshot(q, (snapshot) => {
const notesList = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
setNotes(notesList);
});
}, [user]);

async function handleSignIn() {
await signInWithPopup(auth, new GoogleAuthProvider());
}

async function createNote() {
const docRef = await addDoc(collection(db, 'notes'), {
title: 'Untitled Note',
content: '',
userId: user.uid,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
});
setSelectedNote(docRef.id);
}

async function saveNote(noteId, title, content) {
await updateDoc(doc(db, 'notes', noteId), {
title,
content,
updatedAt: serverTimestamp()
});
}

async function removeNote(noteId) {
await deleteDoc(doc(db, 'notes', noteId));
if (selectedNote === noteId) setSelectedNote(null);
}

if (loading) return <div className="loading">Loading...</div>;

if (!user) {
return (
<div className="auth-container">
<h1>Notes App</h1>
<p>A real-time collaborative notes app built with Firebase.</p>
<button onClick={handleSignIn} className="sign-in-btn">
Sign in with Google
</button>
</div>
);
}

const activeNote = notes.find(n => n.id === selectedNote);

return (
<div className="app">
<div className="sidebar">
<div className="sidebar-header">
<h2>Notes</h2>
<button onClick={createNote}>New Note</button>
</div>

<div className="notes-list">
{notes.map(note => (
<div
key={note.id}
className={note-item ${note.id === selectedNote ? 'active' : ''}}
onClick={() => setSelectedNote(note.id)}
>
<span className="note-title">{note.title || 'Untitled'}</span>
<button
className="delete-btn"
onClick={(e) => { e.stopPropagation(); removeNote(note.id); }}
>
x
</button>
</div>
))}
</div>

<div className="sidebar-footer">
<span>{user.displayName}</span>
<button onClick={() => signOut(auth)}>Sign Out</button>
</div>
</div>

<div className="editor">
{activeNote ? (
<NoteEditor
key={activeNote.id}
note={activeNote}
onSave={saveNote}
/>
) : (
<div className="empty-state">
<p>Select a note or create a new one</p>
</div>
)}
</div>
</div>
);
}

function NoteEditor({ note, onSave }) {
const [title, setTitle] = useState(note.title);
const [content, setContent] = useState(note.content);
const [saveTimer, setSaveTimer] = useState(null);

// Auto-save after 1 second of inactivity
function handleChange(newTitle, newContent) {
setTitle(newTitle);
setContent(newContent);

if (saveTimer) clearTimeout(saveTimer);
const timer = setTimeout(() => {
onSave(note.id, newTitle, newContent);
}, 1000);
setSaveTimer(timer);
}

// Update local state when real-time changes come in
useEffect(() => {
setTitle(note.title);
setContent(note.content);
}, [note.title, note.content]);

return (
<div className="note-editor">
<input
type="text"
value={title}
onChange={(e) => handleChange(e.target.value, content)}
placeholder="Note title"
className="title-input"
/>
<textarea
value={content}
onChange={(e) => handleChange(title, e.target.value)}
placeholder="Start writing..."
className="content-input"
/>
</div>
);
}

export default App;

Deploy the whole thing:

npm run build
firebase deploy

Your app is now live on your-project.web.app with authentication, a real-time database, and CDN hosting. For free.

Common Mistakes

Not setting security rules. Firebase projects start in test mode with rules that allow all reads and writes. This is fine for development but catastrophic for production. Set proper rules before launching. Reading entire collections. Firestore charges per document read. A query that returns 10,000 documents costs 10,000 reads. Use where(), limit(), and pagination. Design your data model to minimize the documents each query needs. Deeply nested subcollections. Firestore queries don't work across subcollections (no "get all notes across all users"). Keep your data structure as flat as possible. Not unsubscribing from listeners. Every onSnapshot returns an unsubscribe function. If you don't call it when the component unmounts, you'll have memory leaks and orphaned connections. Using Firestore like a SQL database. Firestore is NoSQL. Denormalize data. If you need a user's name displayed alongside their notes, store the name on the note document too. Joins are expensive and limited. Think about your query patterns first, then design the data model to support them. Ignoring offline support. Firestore has built-in offline persistence. Writes queue locally and sync when connectivity returns. This is enabled by default on mobile SDKs and can be enabled on web. Design your UI to handle offline state gracefully.

What's Next

You've built a full application with Firebase: authentication, Firestore database, real-time updates, security rules, hosting, and Cloud Functions. From here, explore Firebase Storage for file uploads, Firebase Cloud Messaging for push notifications, and Firestore's aggregation queries for analytics.

Firebase works best when you embrace its model: client-first, real-time, NoSQL. Fight that model and you'll struggle. Lean into it and you'll ship incredibly fast.

Build more projects and level up your development skills at CodeUp.

Ad 728x90