1. What you'll create
In this codelab, you'll be building a traveling blog with a real-time collaborative map with the latest from our Angular library: AngularFire. The final web app will consist of a travel blog where you can upload images to each location that you've traveled to.
AngularFire will be used to build the web app, Emulator Suite for local testing, Authentication to keep track of user data, Firestore and Storage to persist data and media, powered by Cloud Functions, and finally, Firebase Hosting to deploy the app.
What you'll learn
- How to develop with Firebase products locally with Emulator Suite
- How to enhance your web app with AngularFire
- How to persist your data in Firestore
- How to persist media in Storage
- How to deploy your app to Firebase Hosting
- How to use Cloud Functions to interact with your databases and APIs
What you'll need
- Node.js version 10 or higher
- A Google Account for the creation and management of your Firebase Project
- The Firebase CLI version 11.14.2 or later
- A browser of your choice, such as Chrome
- Basic understanding of Angular and Javascript
2. Get the sample code
Clone the codelab's GitHub repository from the command line:
git clone https://github.com/firebase/codelab-friendlychat-web
Alternatively, if you do not have git installed, you can download the repository as a ZIP file.
The Github repository contains sample projects for multiple platforms.
This codelab only uses the webframework repository:
- 📁 webframework: The starting code that you'll build upon during this codelab.
Install dependencies
After cloning, install dependencies in the root and functions
folder before building the web app.
cd webframework && npm install
cd functions && npm install
Install Firebase CLI
Install Firebase CLI using this command in a terminal:
npm install -g firebase-tools
Double check that your Firebase CLI version is greater than 11.14.2 using:
firebase --version
If your version is lower than 11.14.2, please update using:
npm update firebase-tools
3. Create and set up a Firebase project
Create a Firebase project
- Sign in to Firebase.
- In the Firebase console, click Add Project, and then name your Firebase project <your-project>. Remember the project ID for your Firebase project.
- Click Create Project.
Important: Your Firebase project will be named <your-project>, but Firebase will automatically assign it a unique Project ID in the form <your-project>-1234. This unique identifier is how your project is actually identified (including in the CLI), whereas <your-project> is simply a display name.
The application that we're going to build uses Firebase products that are available for web apps:
- Firebase Authentication to easily allow your users to sign into your app.
- Cloud Firestore to save structured data on the cloud and get instant notification when data changes.
- Cloud Storage for Firebase to save files in the cloud.
- Firebase Hosting to host and serve your assets.
- Functions to interact with internal and external APIs.
Some of these products need special configurations or need to be enabled using the Firebase console.
Add a Firebase web app to the project
- Click the web icon to create a new Firebase web app.
- On the next step, you'll see a configuration object. Copy the contents of this object into the
environments/environment.ts
file.
Enable Google sign-in for Firebase Authentication
To allow users to sign in to the web app with their Google accounts, we'll use the Google sign-in method.
To enable Google sign-in:
- In the Firebase console, locate the Build section in the left panel.
- Click Authentication, then click the Sign-in method tab (or click here to go directly there).
- Enable the Google sign-in provider, then click Save.
- Set the public-facing name of your app to <your-project-name> and choose a Project support email from the dropdown menu.
Enable Cloud Firestore
- In the Firebase console's Build section, click Firestore Database.
- Click Create database in the Cloud Firestore pane.
- Set the location where your Cloud Firestore data is stored. You can leave this as the default or choose a region close to you.
Enable Cloud Storage
The web app uses Cloud Storage for Firebase to store, upload, and share pictures.
- In the Firebase console's Build section, click Storage.
- If there's no Get Started button, it means that Cloud storage is already
enabled, and you don't need to follow the steps below.
- Click Get Started.
- Read the disclaimer about security rules for your Firebase project, then click Next.
- The Cloud Storage location is preselected with the same region you chose for your Cloud Firestore database. Click Done to complete the setup.
With the default security rules, any authenticated user can write anything to Cloud Storage. We'll make our storage more secure later in this codelab.
4. Connect to your Firebase project
The Firebase command-line interface (CLI) allows you to use Firebase Hosting to serve your web app locally, as well as to deploy your web app to your Firebase project.
Make sure that your command line is accessing your app's local webframework
directory.
Connect the web app code to your Firebase project. First, log in to the Firebase CLI in command line:
firebase login
Next run the following command to create a project alias. Replace $YOUR_PROJECT_ID
with the ID of your Firebase project.
firebase use $YOUR_PROJECT_ID
Add AngularFire
To add AngularFire to the app, run the command:
ng add @angular/fire
Then, follow the command line instructions, and select the features that exists in your Firebase project.
Initialize Firebase
To initialize the Firebase project, run:
firebase init
Then, following the command line prompts, select the features and emulators that were used in your Firebase project.
Start the emulators
From the webframework
directory, run the following command to start the emulators:
firebase emulators:start
Eventually you should see something like this:
$ firebase emulators:start
i emulators: Starting emulators: auth, functions, firestore, hosting, functions
i firestore: Firestore Emulator logging to firestore-debug.log
i hosting: Serving hosting files from: public
✔ hosting: Local server: http://localhost:5000
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "/functions" for Cloud Functions...
✔ functions[updateMap]: firestore function initialized.
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4000 │
└─────────────────────────────────────────────────────────────┘
┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting │ localhost:5000 │ n/a │
└────────────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
Once you see the ✔All emulators ready!
message, the emulators are ready to use.
You should see your travel app's UI, which is not (yet!) functioning:
Now let's get building!
5. Connect the web app to the emulators
Based on the table in the emulator logs, Cloud Firestore emulator is listening on port 8080 and the Authentication emulator is listening on port 9099.
Open the EmulatorUI
In your web browser, navigate to http://127.0.0.1:4000/. You should see the Emulator Suite UI.
Route the app to use the emulators
In src/app/app.module.ts
, add the following code to AppModule
's list of imports:
@NgModule({
declarations: [...],
imports: [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => {
const auth = getAuth();
if (location.hostname === 'localhost') {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}
return auth;
}),
provideFirestore(() => {
const firestore = getFirestore();
if (location.hostname === 'localhost') {
connectFirestoreEmulator(firestore, '127.0.0.1', 8080);
}
return firestore;
}),
provideFunctions(() => {
const functions = getFunctions();
if (location.hostname === 'localhost') {
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
}
return functions;
}),
provideStorage(() => {
const storage = getStorage();
if (location.hostname === 'localhost') {
connectStorageEmulator(storage, '127.0.0.1', 5001);
}
return storage;
}),
...
]
The app is now configured to use local emulators, allowing testing and development to be done locally.
6. Adding Authentication
Now that emulators are set up for the app, we can add Authentication features to ensure that each user is signed in before they post messages.
To do so, we can import signin
functions directly from AngularFire, and track your user's auth state with the authState
function. Modify the login page functions so that the page checks for user auth state on load.
Injecting AngularFire Auth
In src/app/pages/login-page/login-page.component.ts
, import Auth
from @angular/fire/auth
, and inject it into the LoginPageComponent
. Authentication providers, such as Google, and functions such assignin
, signout
can also be directly imported from the same package, and used in the app.
import { Auth, GoogleAuthProvider, signInWithPopup, signOut, user } from '@angular/fire/auth';
export class LoginPageComponent implements OnInit {
private auth: Auth = inject(Auth);
private provider = new GoogleAuthProvider();
user$ = user(this.auth);
constructor() {}
ngOnInit(): void {}
login() {
signInWithPopup(this.auth, this.provider).then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
return credential;
})
}
logout() {
signOut(this.auth).then(() => {
console.log('signed out');}).catch((error) => {
console.log('sign out error: ' + error);
})
}
}
Now the login page is functional! Try logging in, and check out the results in the Authentication Emulator.
7. Configuring Firestore
In this step, you'll add functionality to post and update travel blog posts stored in Firestore.
Similar to Authentication, Firestore functions come prepackaged from AngularFire. Each document belongs to a collection, and each document can also have nested collections. Knowing the path
of the document in Firestore is required to create and update a travel blog post.
Implementing TravelService
Since many different pages will need to read and update Firestore documents in the web app, we can implement the functions in src/app/services/travel.service.ts
, to refrain from repeatedly injecting the same AngularFire functions every page.
Begin with injecting Auth
, similar to the previous step, as well as Firestore
into our service. Defining an observable user$
object that listens to the current authentication status is also useful.
import { doc, docData, DocumentReference, Firestore, getDoc, setDoc, updateDoc, collection, addDoc, deleteDoc, collectionData, Timestamp } from "@angular/fire/firestore";
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
user$ = authState(this.auth).pipe(filter(user => user !== null), map(user => user!));
router: Router = inject(Router);
Adding a travel post
Travel posts will exist as documents that are stored in Firestore, and since documents must exist within collections, the collection that contains all travel posts will be named travels
. Thus, the path of any travel post will be travels/
Using the addDoc
function from AngularFire, an object can be inserted into a collection:
async addEmptyTravel(userId: String) {
...
addDoc(collection(this.firestore, 'travels'), travelData).then((travelRef) => {
collection(this.firestore, `travels/${travelRef.id}/stops`);
setDoc(travelRef, {... travelData, id: travelRef.id})
this.router.navigate(['edit', `${travelRef.id}`]);
return travelRef;
})
}
Updating and deleting data
Given the uid of any travel post, one can deduce the path of the document stored in Firestore, which can then be read, updated or deleted using AngularFire's updateFoc
and deleteDoc
functions:
async updateData(path: string, data: Partial<Travel | Stop>) {
await updateDoc(doc(this.firestore, path), data)
}
async deleteData(path: string) {
const ref = doc(this.firestore, path);
await deleteDoc(ref)
}
Reading data as an observable
Since travel posts and stops along the way can be modified after creation, it would be more useful to get document objects as observables, to subscribe to any changes that are made. This functionality is offered by the docData
and collectionData
functions from @angular/fire/firestore
.
getDocData(path: string) {
return docData(doc(this.firestore, path), {idField: 'id'}) as Observable<Travel | Stop>
}
getCollectionData(path: string) {
return collectionData(collection(this.firestore, path), {idField: 'id'}) as Observable<Travel[] | Stop[]>
}
Adding stops to a travel post
Now that travel post operations are set up, it's time to consider stops, which will exist under a subcollection of a travel post like so: travels/
This is almost identical to creating a travel post, so challenge yourself to implement it on your own, or check out the implementation below:
async addStop(travelId: string) {
...
const ref = await addDoc(collection(this.firestore, `travels/${travelId}/stops`), stopData)
setDoc(ref, {...stopData, id: ref.id})
}
Nice! The Firestore functions have been implemented in the Travel service, so now you can see them in action.
Using Firestore functions in the app
Navigate to src/app/pages/my-travels/my-travels.component.ts
and inject TravelService
to use its functions.
travelService = inject(TravelService);
travelsData$: Observable<Travel[]>;
stopsList$!: Observable<Stop[]>;
constructor() {
this.travelsData$ = this.travelService.getCollectionData(`travels`) as Observable<Travel[]>
}
TravelService
is called in the constructor to get an Observable array of all travels.
In the case where only the travels of the current user is needed, use the query
function.
Other methods to ensure security include implementing security rules, or using Cloud Functions with Firestore as explored in optional steps below
Then, simply call the functions implemented in TravelService
.
async createTravel(userId: String) {
this.travelService.addEmptyTravel(userId);
}
deleteTravel(travelId: String) {
this.travelService.deleteData(`travels/${travelId}`)
}
Now the My Travels page should be functional! Check out what happens in your Firestore emulator when you create a new travel post.
Then, repeat for the update functions in /src/app/pages/edit-travels/edit-travels.component.ts
:
travelService: TravelService = inject(TravelService)
travelId = this.activatedRoute.snapshot.paramMap.get('travelId');
travelData$: Observable<Travel>;
stopsData$: Observable<Stop[]>;
constructor() {
this.travelData$ = this.travelService.getDocData(`travels/${this.travelId}`) as Observable<Travel>
this.stopsData$ = this.travelService.getCollectionData(`travels/${this.travelId}/stops`) as Observable<Stop[]>
}
updateCurrentTravel(travel: Partial<Travel>) {
this.travelService.updateData(`travels${this.travelId}`, travel)
}
updateCurrentStop(stop: Partial<Stop>) {
stop.type = stop.type?.toString();
this.travelService.updateData(`travels${this.travelId}/stops/${stop.id}`, stop)
}
addStop() {
if (!this.travelId) return;
this.travelService.addStop(this.travelId);
}
deleteStop(stopId: string) {
if (!this.travelId || !stopId) {
return;
}
this.travelService.deleteData(`travels${this.travelId}/stops/${stopId}`)
this.stopsData$ = this.travelService.getCollectionData(`travels${this.travelId}/stops`) as Observable<Stop[]>
}
8. Configuring Storage
You'll now implement Storage to store images and other types of media.
Cloud Firestore is best used to store structured data, such as JSON objects. Cloud Storage is designed to store files or blobs. In this app, you will use it to allow users to share their travel pictures.
Likewise with Firestore, storing and updating files with Storage requires a unique identifier for each file.
Let's implement the functions in TraveService
:
Uploading a file
Navigate to src/app/services/travel.service.ts
and inject Storage from AngularFire:
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
storage: Storage = inject(Storage);
And implement the upload function:
async uploadToStorage(path: string, input: HTMLInputElement, contentType: any) {
if (!input.files) return null
const files: FileList = input.files;
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file) {
const imagePath = `${path}/${file.name}`
const storageRef = ref(this.storage, imagePath);
await uploadBytesResumable(storageRef, file, contentType);
return await getDownloadURL(storageRef);
}
}
return null;
}
The primary difference between accessing documents from Firestore and files from Cloud Storage is that, although they both follow folder structured paths, the base url and path combination is obtained through the getDownloadURL
, which can then be stored, and used in a file.
Using the function in app
Navigate to src/app/components/edit-stop/edit-stop.component.ts
and call the upload function using:
async uploadFile(file: HTMLInputElement, stop: Partial<Stop>) {
const path = `/travels/${this.travelId}/stops/${stop.id}`
const url = await this.travelService.uploadToStorage(path, file, {contentType: 'image/png'});
stop.image = url ? url : '';
this.travelService.updateData(path, stop);
}
When the image is uploaded, the media file itself will be uploaded to storage, and the url stored accordingly in the document in Firestore.
9. Deploying the application
Now we're ready to deploy the application!
Copy the firebase
configs from src/environments/environment.ts
to src/environments/environment.prod.ts
and run:
firebase deploy
You should see something like this:
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.
=== Deploying to 'friendly-travels-b6a4b'...
i deploying storage, firestore, hosting
i firebase.storage: checking storage.rules for compilation errors...
✔ firebase.storage: rules file storage.rules compiled successfully
i firestore: reading indexes from firestore.indexes.json...
i cloud.firestore: checking firestore.rules for compilation errors...
✔ cloud.firestore: rules file firestore.rules compiled successfully
i storage: latest version of storage.rules already up to date, skipping upload...
i firestore: deploying indexes...
i firestore: latest version of firestore.rules already up to date, skipping upload...
✔ firestore: deployed indexes in firestore.indexes.json successfully for (default) database
i hosting[friendly-travels-b6a4b]: beginning deploy...
i hosting[friendly-travels-b6a4b]: found 6 files in .firebase/friendly-travels-b6a4b/hosting
✔ hosting[friendly-travels-b6a4b]: file upload complete
✔ storage: released rules storage.rules to firebase.storage
✔ firestore: released rules firestore.rules to cloud.firestore
i hosting[friendly-travels-b6a4b]: finalizing version...
✔ hosting[friendly-travels-b6a4b]: version finalized
i hosting[friendly-travels-b6a4b]: releasing new version...
✔ hosting[friendly-travels-b6a4b]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendly-travels-b6a4b/overview
Hosting URL: https://friendly-travels-b6a4b.web.app
10. Congratulations!
Now your application should be complete and deployed to Firebase Hosting! All data and analytics will now be accessible in your Firebase Console.
For more features regarding AngularFire, Functions, security rules, don't forget to check out optional steps below, as well as other Firebase Codelabs !
11. Optional: AngularFire auth guards
Along with Firebase Authentication, AngularFire also offers authentication based guards on routes, so that users with insufficient access can be redirected. This helps protect the app from users accessing protected data.
In src/app/app-routing.module.ts
, import
import {AuthGuard, redirectLoggedInTo, redirectUnauthorizedTo} from '@angular/fire/auth-guard'
You can then define functions as to when, and where users should be redirected to on certain pages:
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['signin']);
const redirectLoggedInToTravels = () => redirectLoggedInTo(['my-travels']);
Then simply add them to your routes:
const routes: Routes = [
{path: '', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'signin', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'my-travels', component: MyTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
{path: 'edit/:travelId', component: EditTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
];
12. Optional: security rules
Both Firestore and Cloud Storage use security rules (firestore.rules
and security.rules
respectively) to enforce security and to validate data.
At the moment, the Firestore and Storage data has open access for reads and writes, but you don't want people to go about changing others' posts! You can use security rules to restrict access to your collections and documents.
Firestore rules
To only allow authenticated users to view travel posts, go to firestore.rules
file and add:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/travels {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
Security rules can also be used to validate data:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/posts {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
&& "author" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
Storage rules
Similarly, we can use security rules to enforce access to storage databases in storage.rules
. Note that we can also use functions for more complex checks:
rules_version = '2';
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{postId}/{filename} {
allow write: if request.auth != null
&& request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}