Step-by-step guide to scaffold, connect, and deploy a real-world Angular app with Firebase Auth & Firestore.
TL;DR:
In ~60 minutes you’ll scaffold an Angular 17 app, wire Firebase Auth (Email/Password + Google), build a Firestore CRUD “Tasks” feature, protect routes with a functional guard, and deploy to Firebase Hosting.
- Step-by-step guide to scaffold, connect, and deploy a real-world Angular app with Firebase Auth & Firestore.
- TL;DR:
- What you’ll learn
- Prerequisites
- 1) Scaffold an Angular 17 project
- 2) Add Firebase to your Angular app (AngularFire)
- 3) Enable Firebase products
- 4) Wire Firebase providers in app.config.ts
- 5) Define routes + a functional Auth guard
- 6) Build the Auth UI (Email/Password + Google)
- 7) Create a Firestore “tasks” feature (CRUD)
- 8) Secure your data with Firestore Rules (development → production)
- 9) Environment configuration (Firebase keys)
- 10) Deploy to Firebase Hosting
- Common pitfalls (and fixes)
- References & Further Reading
What you’ll learn
- Angular 17 standalone setup + routing
- Firebase Auth: email/password + Google sign-in
- Firestore CRUD with real-time streams
- Basic, safer Firestore Security Rules for user-owned data
- One-command deploy to Firebase Hosting
Prerequisites
- Node.js 18+ and npm
- A Google account (for Firebase)
- Angular CLI and Firebase CLI installed
npm install -g @angular/cli
npm install -g firebase-tools
References: Angular installation & CLI angular.dev+1 • Firebase Hosting quickstart Firebase
1) Scaffold an Angular 17 project
ng new angular-firebase-crud --routing --style=scss
cd angular-firebase-crud
ng serve -o
Angular 17 uses standalone components by default (no NgModules). It keeps things light and fast.
2) Add Firebase to your Angular app (AngularFire)
Install the Firebase Web SDK + AngularFire, then run the schematic:
npm i firebase @angular/fire
ng add @angular/fire
The ng add wizard:
- selects your Firebase project
- writes Firebase config into your environments
- can set up Hosting/Firestore/Auth for you
Why AngularFire? It’s the official Angular wrapper that exposes providers for Auth, Firestore, etc., matching Angular’s reactive style.
3) Enable Firebase products
In the Firebase console:
- Authentication → enable Email/Password and (optionally) Google
- Firestore → create a database (start in test mode only during dev)
Auth (web) quickstart + Google sign-in steps. Firebase+1
Adding/updating Firestore data (modular v9+ API). Firebase
4) Wire Firebase providers in app.config.ts
Angular 17 boots via main.ts + app.config.ts. Register Firebase providers once:
// src/app/app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
// AngularFire providers (modular)
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
import { provideAuth, getAuth } from '@angular/fire/auth';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';
import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
importProvidersFrom(
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore())
)
],
};
AngularFire provider pattern on npm (modern examples). npm
5) Define routes + a functional Auth guard
Create routes for login and tasks:
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { canActivateAuth } from './shared/auth.guard';
export const routes: Routes = [
{ path: 'login', loadComponent: () => import('./auth/login.component').then(m => m.LoginComponent) },
{ path: 'tasks', loadComponent: () => import('./tasks/tasks.component').then(m => m.TasksComponent), canActivate: [canActivateAuth] },
{ path: '', pathMatch: 'full', redirectTo: 'tasks' },
{ path: '**', redirectTo: 'tasks' },
];
Create a functional guard that checks Firebase Auth state:
// src/app/shared/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { Auth, onAuthStateChanged } from '@angular/fire/auth';
export const canActivateAuth: CanActivateFn = () =>
new Promise<boolean>(resolve => {
const auth = inject(Auth);
const router = inject(Router);
const unsub = onAuthStateChanged(auth, user => {
unsub();
if (user) resolve(true);
else {
router.navigateByUrl('/login');
resolve(false);
}
});
});
Functional guards (CanActivateFn) are the modern way to protect routes. angular.dev+1
6) Build the Auth UI (Email/Password + Google)
Login component (standalone):
// src/app/auth/login.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Auth, signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from '@angular/fire/auth';
import { Router } from '@angular/router';
@Component({
standalone: true,
imports: [CommonModule],
selector: 'app-login',
template: `
<div class="auth-card">
<h1>Sign in</h1>
<form (submit)="emailLogin($event)">
<input type="email" placeholder="Email" [(ngModel)]="email()" name="email" required />
<input type="password" placeholder="Password" [(ngModel)]="password()" name="password" required />
<button type="submit">Sign in</button>
</form>
<button (click)="googleLogin()">Continue with Google</button>
</div>
`,
styles: [`.auth-card{max-width:420px;margin:3rem auto;display:grid;gap:.75rem}`]
})
export class LoginComponent {
email = signal('');
password = signal('');
constructor(private auth: Auth, private router: Router) {}
async emailLogin(e: Event) {
e.preventDefault();
await signInWithEmailAndPassword(this.auth, this.email(), this.password());
this.router.navigateByUrl('/tasks');
}
async googleLogin() {
await signInWithPopup(this.auth, new GoogleAuthProvider());
this.router.navigateByUrl('/tasks');
}
}
Add a simple logout control anywhere (e.g., header):
import { Auth, signOut } from '@angular/fire/auth';
// ...
<button (click)="signOut(auth)">Logout</button>
Email/Password start & Google provider docs. Firebase+1
7) Create a Firestore “tasks” feature (CRUD)
Model + service
// src/app/tasks/task.model.ts
export interface Task {
id?: string;
title: string;
done: boolean;
uid: string; // owner
createdAt: any; // Firestore Timestamp
}
// src/app/tasks/task.service.ts
import { Injectable, inject } from '@angular/core';
import { Auth } from '@angular/fire/auth';
import { Firestore, collection, collectionData, addDoc, doc, updateDoc, deleteDoc, query, where, serverTimestamp } from '@angular/fire/firestore';
import { Task } from './task.model';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class TaskService {
private afs = inject(Firestore);
private auth = inject(Auth);
private tasksCol() {
const uid = this.auth.currentUser?.uid;
return collection(this.afs, 'tasks');
}
listMyTasks() {
const uid = this.auth.currentUser?.uid!;
const q = query(this.tasksCol(), where('uid', '==', uid));
// include id field for UI updates
return collectionData(q, { idField: 'id' }) as any;
}
async add(title: string) {
const uid = this.auth.currentUser?.uid!;
await addDoc(this.tasksCol(), { title, done: false, uid, createdAt: serverTimestamp() });
}
async toggleDone(task: Task) {
await updateDoc(doc(this.afs, `tasks/${task.id}`), { done: !task.done });
}
async remove(task: Task) {
await deleteDoc(doc(this.afs, `tasks/${task.id}`));
}
}
Firestore CRUD (addDoc, updateDoc, deleteDoc) with the modular SDK; collectionData gives you a real-time stream for your list. Firebase+2Firebase+2
Tasks component
// src/app/tasks/tasks.component.ts
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TaskService } from './task.service';
import { Observable } from 'rxjs';
import { Task } from './task.model';
@Component({
standalone: true,
imports: [CommonModule],
selector: 'app-tasks',
template: `
<section class="wrap">
<header>
<h1>My Tasks</h1>
<form (submit)="create($event)">
<input type="text" placeholder="New task..." [(ngModel)]="title()" name="title" required />
<button type="submit">Add</button>
</form>
</header>
<ul>
<li *ngFor="let t of tasks$ | async">
<label>
<input type="checkbox" [checked]="t.done" (change)="toggle(t)" />
<span [class.done]="t.done">{{ t.title }}</span>
</label>
<button (click)="remove(t)">✕</button>
</li>
</ul>
</section>
`,
styles: [`
.wrap{max-width:680px;margin:2rem auto}
header{display:flex;gap:.5rem;align-items:center}
ul{list-style:none;padding:0}
li{display:flex;justify-content:space-between;align-items:center;padding:.5rem 0;border-bottom:1px solid #eee}
.done{text-decoration:line-through;opacity:.7}
`]
})
export class TasksComponent {
private svc = inject(TaskService);
title = signal('');
tasks$: Observable<Task[]> = this.svc.listMyTasks();
async create(e: Event){ e.preventDefault(); await this.svc.add(this.title()); this.title.set(''); }
toggle(t: Task){ this.svc.toggleDone(t); }
remove(t: Task){ this.svc.remove(t); }
}
8) Secure your data with Firestore Rules (development → production)
During development, it’s common to start permissive, but do not ship permissive rules. Here’s a minimal “user owns their tasks” set:
// Firestore rules (Rules tab)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{taskId} {
allow create: if request.auth != null
&& request.resource.data.uid == request.auth.uid;
allow read, update, delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
}
}
}
This allows each authenticated user to create/list/update/delete only their own tasks. Learn more about Firestore rules and why auth != null alone is not enough for production. Firebase+1
9) Environment configuration (Firebase keys)
Angular builds can swap files per environment using fileReplacements in angular.json (environment.ts vs environment.prod.ts). Keep secrets server-side whenever possible—Firebase client config isn’t secret. angular.dev
10) Deploy to Firebase Hosting
Initialize Hosting and deploy:
firebase login
firebase init hosting # choose your project, set "dist/angular-firebase-crud/browser" as public dir after "ng build"
ng build --configuration production
firebase deploy --only hosting
Alternatively, use the framework-aware integration which auto-detects Angular. Firebase+1
Common pitfalls (and fixes)
- Guard fires before Auth state is restored on refresh: use a functional guard that waits on
onAuthStateChanged(as shown). angular.dev - Mixing legacy and modular imports: prefer modular APIs via
@angular/fire/*&firebase/*as shown. npm - Over-permissive rules: don’t ship
allow read, write: if request.auth != nullglobally. Lock byuid. Google Cloud
References & Further Reading
- Angular install & CLI docs; build/deploy basics. angular.dev+2angular.io+2
- Angular standalone components (v17) & routing guards (functional). angular.io+1
- AngularFire (official) and provider setup examples. GitHub+1
- Firebase Auth (web) quickstart and Google sign-in. Firebase+1
- Firestore CRUD with the modular SDK (addDoc/updateDoc/deleteDoc/get). Firebase+1
- Firestore Security Rules (basics + getting started). Firebase+1
- Firebase Hosting quickstart & Angular framework integration. Firebase+1

