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 Authenticationto easily allow your users to sign into your app.
- Cloud Firestoreto save structured data on the cloud and get instant notification when data changes.
- Cloud Storage for Firebaseto save files in the cloud.
- Firebase Hostingto host and serve your assets.
- Functionsto 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 Googlesign-in method.
To enable Googlesign-in:
- In the Firebase console, locate the Buildsection in the left panel.
- Click Authentication, then click the Sign-in methodtab (or click here to go directly there).
- Enable the Googlesign-in provider, then click Save.
- Set the public-facing name of your app to <your-project-name>and choose a Project support emailfrom the dropdown menu.
Enable Cloud Firestore
- In the Firebase console's Buildsection, click Firestore Database.
- Click Create databasein 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 Buildsection, click Storage.
- If there's no Get Startedbutton, 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 Doneto 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 as signin
, 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}/s
tops
`
)
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
;
}
}
}