Featured image showing ‘Angular 17 & Firebase’ with a stylized Angular hexagon, Firebase flame, and a database icon connected, on a dark gradient background; caption ‘Build a CRUD App (Auth + Firestore + Hosting)’

Build a CRUD App with Angular 17 & Firebase

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.

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 != null globally. Lock by uid. 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

Leave a Comment

Your email address will not be published. Required fields are marked *