This guide connects an Angular app to Firebase and builds the two things most apps need first: user authentication (email/password and Google) with protected routes, and per-user data in Cloud Firestore. It uses current Angular (standalone components, signals, functional guards) and the modular Firebase JavaScript SDK.
It replaces two older posts on this site, one on connecting Firebase to Angular and one on Firebase Authentication in Angular, which overlapped and were both built on APIs that no longer exist. Both also called the framework “AngularJS”, which is the separate, discontinued 1.x framework; everything here is modern Angular.
What I tested: I generated a project with Angular CLI 22.2 and Firebase JS SDK 13.0. It builds, and three specs run by ng test (Vitest) against the Firebase Auth and Firestore emulators cover sign-up, sign-out, a failed and a successful sign-in, adding, streaming and deleting notes, and a Security Rules denial. Google sign-in uses a browser popup, so it was not part of those tests.
AngularFire or the Firebase SDK Directly?
AngularFire, the official Angular library for Firebase, wraps the SDK in Angular-friendly APIs. It is a good choice when its version matches your Angular version, but it often lags behind Angular releases. When I tried it, AngularFire 20.1.1 declared a peer dependency on Angular 20, and npm refused to install it into the Angular 22 project:
$ npm i @angular/[email protected]
npm error code ERESOLVE
npm error Could not resolve dependency:
npm error peer @angular/common@"^20.0.0" from @angular/[email protected]Rather than force it with --legacy-peer-deps, this guide uses the Firebase SDK directly and provides it through Angular’s dependency injection. That takes about 20 lines, works with any Angular version and is easy to test. If your Angular version matches a released AngularFire, using AngularFire’s provideFirebaseApp, provideAuth and provideFirestore is a fine alternative.
Step 1: Create the Firebase Project
- In the Firebase console, create a project.
- In Authentication > Sign-in method, enable Email/Password and Google.
- In Firestore Database, create a database.
- In Project settings > Your apps, add a Web app and copy its config.
Step 2: Create the Angular App
npx @angular/cli@latest new fire-app --style=css --ssr=false
cd fire-app
npm install firebase
npm install -D firebase-toolsAngular 22 requires Node.js 22.22.3+ or 24.15+. Put the web config in a file; it identifies your project and is not a secret (your data is protected by Security Rules, not by hiding the config):
import type { FirebaseOptions } from 'firebase/app';
// From Firebase console > Project settings > Your apps. This config is not secret.
export const firebaseConfig: FirebaseOptions = {
apiKey: 'your-api-key',
authDomain: 'your-project.firebaseapp.com',
projectId: 'your-project',
appId: 'your-app-id',
};
export const useEmulators = false;Step 3: Provide Firebase Through Dependency Injection
This file initializes Firebase once and registers the Auth and Firestore instances as injection tokens. It can also point them at the local emulators:
import { InjectionToken, makeEnvironmentProviders } from '@angular/core';
import { initializeApp, type FirebaseOptions } from 'firebase/app';
import { connectAuthEmulator, getAuth, type Auth } from 'firebase/auth';
import { connectFirestoreEmulator, getFirestore, type Firestore } from 'firebase/firestore';
export const FIREBASE_AUTH = new InjectionToken<Auth>('Firebase Auth');
export const FIRESTORE = new InjectionToken<Firestore>('Firestore');
// Registers Firebase once for the whole app. Components and services
// then inject FIREBASE_AUTH and FIRESTORE instead of importing globals.
export function provideFirebase(options: FirebaseOptions, useEmulators = false) {
const app = initializeApp(options);
const auth = getAuth(app);
const db = getFirestore(app);
if (useEmulators) {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
connectFirestoreEmulator(db, '127.0.0.1', 8080);
}
return makeEnvironmentProviders([
{ provide: FIREBASE_AUTH, useValue: auth },
{ provide: FIRESTORE, useValue: db },
]);
}import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideFirebase } from './core/firebase';
import { firebaseConfig, useEmulators } from './firebase.config';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideFirebase(firebaseConfig, useEmulators),
],
};Step 4: An Auth Service With Signals
The service exposes the current user as a signal. The state starts as undefined until Firebase has restored any saved session, so the app can tell “still checking” apart from “signed out”:
import { Injectable, computed, inject, signal } from '@angular/core';
import {
GoogleAuthProvider,
createUserWithEmailAndPassword,
onAuthStateChanged,
sendPasswordResetEmail,
signInWithEmailAndPassword,
signInWithPopup,
signOut,
updateProfile,
type User,
} from 'firebase/auth';
import { FIREBASE_AUTH } from './firebase';
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly auth = inject(FIREBASE_AUTH);
// undefined = Firebase has not restored the session yet; null = signed out.
private readonly state = signal<User | null | undefined>(undefined);
readonly user = computed(() => this.state() ?? null);
readonly ready = computed(() => this.state() !== undefined);
/** Resolves once the initial session check is done. */
readonly whenReady: Promise<void>;
constructor() {
this.whenReady = new Promise(resolve => {
onAuthStateChanged(this.auth, user => {
this.state.set(user);
resolve();
});
});
}
async signUp(name: string, email: string, password: string) {
const { user } = await createUserWithEmailAndPassword(this.auth, email, password);
await updateProfile(user, { displayName: name });
return user;
}
signIn(email: string, password: string) {
return signInWithEmailAndPassword(this.auth, email, password);
}
signInWithGoogle() {
return signInWithPopup(this.auth, new GoogleAuthProvider());
}
resetPassword(email: string) {
return sendPasswordResetEmail(this.auth, email);
}
signOut() {
return signOut(this.auth);
}
}
export function authErrorMessage(err: unknown): string {
switch ((err as { code?: string })?.code) {
case 'auth/invalid-credential':
case 'auth/wrong-password':
case 'auth/user-not-found':
return 'Email or password is incorrect.';
case 'auth/email-already-in-use':
return 'An account with this email already exists.';
case 'auth/weak-password':
return 'Password must be at least 6 characters.';
case 'auth/popup-closed-by-user':
return 'Sign-in was cancelled.';
default:
return 'Something went wrong. Please try again.';
}
}Projects created since September 2023 report a wrong password or unknown email as auth/invalid-credential; older projects and the emulator still use the older codes, so the error mapping handles all of them. Use signInWithPopup rather than signInWithRedirect, which breaks in browsers that block third-party storage unless you serve Firebase’s auth handler from your own domain.
Step 5: Protect Routes With a Functional Guard
The guard waits for the session check, then either allows the route or redirects to the login page with a return URL:
import { inject } from '@angular/core';
import { Router, type CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
// Waits for Firebase to restore the session, then allows or redirects.
export const authGuard: CanActivateFn = async (_route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
await auth.whenReady;
return auth.user() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};import { Routes } from '@angular/router';
import { authGuard } from './core/auth.guard';
export const routes: Routes = [
{ path: '', canActivate: [authGuard], loadComponent: () => import('./pages/notes').then(m => m.NotesPage) },
{ path: 'login', loadComponent: () => import('./pages/login').then(m => m.LoginPage) },
{ path: '**', redirectTo: '' },
];Step 6: Store Per-User Data in Firestore
Notes live under users/{uid}/notes, which makes the Security Rules simple. onSnapshot is wrapped in an RxJS Observable so components can turn it into a signal with toSignal:
import { Injectable, inject } from '@angular/core';
import {
addDoc, collection, deleteDoc, doc, onSnapshot, orderBy, query, serverTimestamp,
} from 'firebase/firestore';
import { Observable } from 'rxjs';
import { FIRESTORE } from './firebase';
export type Note = { id: string; text: string };
// Notes live under users/{uid}/notes, so Security Rules can restrict them to their owner.
@Injectable({ providedIn: 'root' })
export class NotesService {
private readonly db = inject(FIRESTORE);
watch(uid: string): Observable<Note[]> {
const q = query(collection(this.db, 'users', uid, 'notes'), orderBy('createdAt', 'desc'));
return new Observable(subscriber =>
onSnapshot(
q,
snap => subscriber.next(snap.docs.map(d => ({ id: d.id, text: d.get('text') as string }))),
err => subscriber.error(err),
),
);
}
add(uid: string, text: string) {
return addDoc(collection(this.db, 'users', uid, 'notes'), { text, createdAt: serverTimestamp() });
}
remove(uid: string, id: string) {
return deleteDoc(doc(this.db, 'users', uid, 'notes', id));
}
}The Security Rules that go with it:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Each user can only read and write their own notes.
match /users/{uid}/notes/{noteId} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}For the same pattern in React, see how to use Cloud Firestore in a React app.
Step 7: The Pages
Login and sign-up
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService, authErrorMessage } from '../core/auth.service';
@Component({
selector: 'app-login',
imports: [FormsModule],
template: `
<h1>{{ mode() === 'login' ? 'Log in' : 'Create an account' }}</h1>
<form (ngSubmit)="submit()">
@if (mode() === 'signup') {
<label>Name <input name="name" [(ngModel)]="name" required /></label>
}
<label>Email <input name="email" type="email" [(ngModel)]="email" autocomplete="email" required /></label>
<label>Password <input name="password" type="password" [(ngModel)]="password" minlength="6" required /></label>
<button type="submit">{{ mode() === 'login' ? 'Log in' : 'Sign up' }}</button>
</form>
<button (click)="google()">Continue with Google</button>
@if (mode() === 'login') {
<button (click)="reset()">Forgot password?</button>
}
<button (click)="mode.set(mode() === 'login' ? 'signup' : 'login')">
{{ mode() === 'login' ? 'Need an account? Sign up' : 'Have an account? Log in' }}
</button>
@if (message()) {
<p role="alert">{{ message() }}</p>
}
`,
})
export class LoginPage {
private readonly auth = inject(AuthService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
readonly mode = signal<'login' | 'signup'>('login');
readonly message = signal<string | null>(null);
name = '';
email = '';
password = '';
private async run(action: () => Promise<unknown>) {
this.message.set(null);
try {
await action();
await this.router.navigateByUrl(this.route.snapshot.queryParamMap.get('returnUrl') ?? '/');
} catch (err) {
this.message.set(authErrorMessage(err));
}
}
submit() {
return this.mode() === 'login'
? this.run(() => this.auth.signIn(this.email, this.password))
: this.run(() => this.auth.signUp(this.name, this.email, this.password));
}
google() {
return this.run(() => this.auth.signInWithGoogle());
}
async reset() {
if (!this.email) return this.message.set('Enter your email first.');
await this.auth.resetPassword(this.email).catch(() => {});
this.message.set('If an account exists for that email, a reset link is on its way.');
}
}Notes
import { Component, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { AuthService } from '../core/auth.service';
import { NotesService } from '../core/notes.service';
@Component({
selector: 'app-notes',
imports: [FormsModule],
template: `
<h1>Hello, {{ auth.user()?.displayName ?? auth.user()?.email }}</h1>
<form (ngSubmit)="add()">
<input name="text" [(ngModel)]="text" placeholder="New note" aria-label="New note" />
<button type="submit">Add</button>
</form>
<ul>
@for (note of notes(); track note.id) {
<li>{{ note.text }} <button (click)="remove(note.id)">Delete</button></li>
} @empty {
<li>No notes yet.</li>
}
</ul>
<button (click)="logout()">Log out</button>
`,
})
export class NotesPage {
protected readonly auth = inject(AuthService);
private readonly notesService = inject(NotesService);
private readonly router = inject(Router);
private readonly uid = this.auth.user()!.uid; // the guard guarantees a user
protected readonly notes = toSignal(this.uid ? this.notesService.watch(this.uid) : of([]), { initialValue: [] });
protected text = '';
async add() {
if (!this.text.trim()) return;
await this.notesService.add(this.uid, this.text.trim());
this.text = '';
}
remove(id: string) {
return this.notesService.remove(this.uid, id);
}
async logout() {
await this.auth.signOut();
await this.router.navigateByUrl('/login');
}
}import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `<main><router-outlet /></main>`,
})
export class App {}The pages are lazy-loaded, but Firebase itself still adds weight: the production build exceeded Angular’s default 500 kB initial-bundle budget. If that matters for your app, initialize Firestore lazily inside the feature that uses it, or raise the budget deliberately.
Step 8: Test Against the Emulators
The Firebase Local Emulator Suite runs Auth and Firestore locally, so tests create real accounts and documents without touching production. This firebase.json enables them:
{
"firestore": { "rules": "firestore.rules" },
"emulators": {
"auth": { "port": 9099 },
"firestore": { "port": 8080 },
"ui": { "enabled": false },
"singleProjectMode": true
}
}The spec uses Angular’s TestBed with provideFirebase(..., true), so the real services run against the emulators:
import { TestBed } from '@angular/core/testing';
import { firstValueFrom, filter } from 'rxjs';
import { describe, expect, it, beforeAll } from 'vitest';
import { provideFirebase } from './firebase';
import { AuthService, authErrorMessage } from './auth.service';
import { NotesService } from './notes.service';
// Runs against the Auth and Firestore emulators (firebase emulators:exec).
describe('AuthService + NotesService', () => {
let auth: AuthService;
let notes: NotesService;
beforeAll(() => {
TestBed.configureTestingModule({
providers: [provideFirebase({ apiKey: 'demo-key', projectId: 'demo-angular' }, true)],
});
auth = TestBed.inject(AuthService);
notes = TestBed.inject(NotesService);
});
it('signs up, signs out and signs back in', async () => {
await auth.whenReady;
const email = `ng${Date.now()}@example.com`;
const user = await auth.signUp('Grace', email, 'secret123');
expect(user.displayName).toBe('Grace');
expect(auth.user()?.email).toBe(email);
await auth.signOut();
expect(auth.user()).toBeNull();
const err = await auth.signIn(email, 'wrong-password').catch(e => e);
expect(authErrorMessage(err)).toBe('Email or password is incorrect.');
await auth.signIn(email, 'secret123');
expect(auth.user()?.email).toBe(email);
});
it('adds, streams and deletes the signed-in user’s notes', async () => {
const uid = auth.user()!.uid;
const ref = await notes.add(uid, 'First note');
const list = await firstValueFrom(notes.watch(uid).pipe(filter(n => n.length === 1)));
expect(list[0].text).toBe('First note');
await notes.remove(uid, ref.id);
await firstValueFrom(notes.watch(uid).pipe(filter(n => n.length === 0)));
});
it('Security Rules block another user’s notes', async () => {
await expect(notes.add('someone-else', 'sneaky')).rejects.toThrow();
});
});$ npx firebase emulators:exec --only auth,firestore --project demo-angular "npx ng test --watch=false"
Test Files 1 passed (1)
Tests 3 passed (3)Project IDs that start with demo- keep the emulators from touching any real project. To run the app itself against them, set useEmulators = true and start npx firebase emulators:start.
Deploying
- Build with
ng buildand deploy to Firebase Hosting (firebase init hosting, public folderdist/fire-app/browser, single-page app rewrites on) or any static host. - Add your production domain under Authentication > Settings > Authorized domains.
- Deploy your Firestore rules with
firebase deploy --only firestore:rules, and never leave a database in test mode.
Conclusion
Connecting modern Angular to Firebase takes three small pieces: a provider that initializes the SDK and exposes it through dependency injection, an auth service built on signals with a functional guard, and Firestore services that return Observables. Keep security in Firestore rules, test against the emulators, and use AngularFire when it supports your Angular version. For the same setup in React, see Firebase Authentication with React.
FAQs
Do I need AngularFire to use Firebase with Angular?
No. AngularFire adds Angular-friendly wrappers, but you can use the modular Firebase JavaScript SDK directly and provide it through Angular dependency injection, which also avoids waiting for AngularFire to support a new Angular version.
Is this for AngularJS?
No. AngularJS is the discontinued 1.x framework. This guide is for modern Angular with standalone components and signals.
How do I protect routes in Angular with Firebase Auth?
Use a functional CanActivate guard that waits for Firebase to restore the session, then returns true for signed-in users or a UrlTree to the login page for everyone else.
Is it safe to put the Firebase config in the Angular app?
Yes. The web config identifies your project and is meant to be public. Protect data with Firestore and Storage Security Rules and restrict sign-in domains.
How do I test Firebase code in Angular?
Run the Firebase Auth and Firestore emulators with firebase emulators:exec and run ng test inside it, providing Firebase connected to the emulators in TestBed.
Why does Google sign-in with redirect fail?
signInWithRedirect relies on third-party storage, which modern browsers block. Use signInWithPopup, or serve Firebase’s auth handler from your own domain.








