IMHO, those two concepts are not intuitive. Out-of-the-box, you start with creating a project which can contain most of the promised features (e.g., hosting, DB). Those features are sitting at the project level, but that's actually not the best way to manage them. Though you are not forced to, you should be using Apps inside your project, and then use the Firebase features under each app. This allows, for example, to seperate your project in different environment (e.g., test, prod). To create a new App in a project:
- Log in to your project and click on
Project Overview
. - Click on the
Add app
button.
npm install -g firebase-tools
Once it is installed, use the firebase
command to manage the deployments and the configuration of your project.
NOTE: This tool can also be installed locally in each project to improve portability. Instead of installing the CLI with the command above, use:
npm install firebase-tools --save-dev
However, with this approach, the
firebase
command is not accessible globally. To automate the project management via this tool, you will have to script it via thepackage.json
scripts
property.
Command | Description |
---|---|
firebase login |
Connects the Firebase CLI to a Firebase account. |
firebase logout |
Disconnects the Firebase CLI from the current Firebase account. |
firebase login:list |
Checks the identity of the current conected Firebase account. |
firebase projects:list |
Lists all the projects for the current connected Firebase account. |
firebase deploy |
Deploys a website to the currently connected Firebase account. |
Run the following command, and answer the questions:
firebase init
This will create 3 files and at least 1 folder:
.firebaserc
: Defines which firebase project you’re deploying your website to.database.rules.json
: DB rules.firebase.json
: Defines which firebase project you’re deploying your website to.public
folder: This is the folder where you should put all your static HTML..firebaserc
:
{
"projects": {
"default": "vexd-website"
}
}
Where:
projects
defines all the Firebase Hosting projects where your website can be deployed.default
is an alias that is the default alias used when you deploy using the firebase deploy command.vexd-website
is our firebase project id. You can find it in your firebase online console under Hosting (this is the https://vexd-website.firebaseapp.com).
This is how you configure your project so you can deploy it to different environment:
- Create new hosting environment in Firebase (e.g. quivers-admin-test for test, quivers-admin-demo for demo, quivers-admin-prod for prod).
- Define an alias for each of the above environment in your .firebaserc file.
{
"projects": {
"default": "vexd-website",
"test": "quivers-admin-test",
"demo": "quivers-admin-demo",
"prod": "quivers-admin-prod"
}
}
To switch to any a specific environment, use the following command:
firebase use test
To deploy to that specific environment, use:
firebase deploy
Update the firebase.json
as follow:
{
"database": {
"rules": "database.rules.json"
},
"hosting": {
"public": "_site",
"ignore": [
"firebase.json",
"firebase-debug.log",
"**/.*"
]
}
firebase deploy
If you want to set up a specific environment, or deploy to a specific environment, use aliases as described in the Multiple environment with aliases section.
You simply need to deploy something on your firebase project first (cf. previous Deploying To Firebase).
Once you’ve deployed something on your firebase hosting, firebase will easily provide you 2 TXT Records files:
- Click on
Hosting
for your specific project. - Click on
Custom Domain
, and there enter your custom domain, then clickVerify
. - Add your 2 TXT records to your DNS. Browse to your domain provider (e.g. GoDaddy), and add those 2 new records for your specific domain.
- Wait a few hours before Google confirms that your custom domain has been verified.
WARNING: To test web notifications in your browser, make sure the web notifications have been turned on in your OS. To know more about this topic, please refer to the How to turn web notifications on/off in MacOS? section.
Firebase hosts a proprietary Pub/Sub BaaS called the Firebase Communication Messaging (FCM). The recommended architecture to build push notifications with Firebase requires three components:
- The FCM
- The Firebase SDK installed and configured on each client (i.e., Android, iOS, Web).
- The Firebase SDK installed integrated in your backend logic to push notifications based on certain events.
Each client (#2) can subscribe to specific topics. The server can also contact them privately using their unique REGISTRATION ID
. In that case, your logic needs to be able to associate REGISTRATION IDs
to users in your own data store. When your logic needs to notify a specific user, it performs a lookup to determine the user's REGISTRATION IDs
so it knows where to push the notification.
- Login to your Firebase account and select/create your project.
- If you already have an app in your project, select it. Otherwise, create one as follow:
- In the default project page, click on the
Add app
button. - Select a platform. Unless you're aiming to use that app for other purposes than puch notifications, it does not matter which platform you use. Push notifications are uses the same architecture regardless of the platform.
- Fill up the details and then click the
Register app
button.
- In the default project page, click on the
- Select your app and click on the cog wheel to navigate to the app page.
- In the app page, select the
Cloud Messaging
tab.
- Install the Firebase Admin SDK
npm install firebase-admin
- In your code, initialize the Firebase Admin object using one the following four official strategies + 1 that personal approach that I found more portable:
- Using a
service-account.json
file:- To acquire this credentials file, please refer to the How to get the service account JSON file? section.
- Set up an
GOOGLE_APPLICATION_CREDENTIALS
environment variable that contains the absolute path to the service account JSON file on your local machine. This variable is automatically set up in Firebase Cloud Function. - Initialize the admin SDK in your code as follow:
const firebase = require('firebase-admin') firebase.initializeApp({ credential: firebase.credential.applicationDefault(), databaseURL: 'https://<DATABASE_NAME>.firebaseio.com' })
- Explicitly defining the creds in your code (deprecated as it could expose accesses to your account):
const firebase = require('firebase-admin') const serviceAccount = { "type": "service_account", "project_id": "YOUR-PROJECT-ID", "private_key_id": "***************", "private_key": "-----BEGIN PRIVATE KEY-----\n***********\n-----END PRIVATE KEY-----\n", "client_email": "***********.iam.gserviceaccount.com", "client_id": "***************", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/*********.iam.gserviceaccount.com" } firebase.initializeApp({ credential: firebase.credential.cert(serviceAccount), databaseURL: 'https://<DATABASE_NAME>.firebaseio.com' })
- Using an OAUTH2 refresh token:
const firebase = require('firebase-admin') const refreshToken = '*************' firebase.initializeApp({ credential: firebase.credential.refreshToken(refreshToken), databaseURL: 'https://<DATABASE_NAME>.firebaseio.com' })
- Using a
FIREBASE_CONFIG
and anGOOGLE_APPLICATION_CREDENTIALS
environment variables:- This
FIREBASE_CONFIG
variable is automatically set up in Firebase Cloud Function. This variable can either contain a JSON string or the absolute path to the config. This config contains all the information except thecredential
which is defined by theGOOGLE_APPLICATION_CREDENTIALS
. - Initialize the admin SDK in your code as follow:
const firebase = require('firebase-admin') firebase.initializeApp()
- This
- Base64 encode the service account JSON file into an environment variable:
const credsBase64 = Buffer.from(JSON.stringify(creds)).toString('base64')
- Store that value in an environment variable
GOOGLE_FIREBASE_KEY
- Use this environment variable to configure the firebase SDK:
const firebase = require('firebase-admin') const serviceAccount = JSON.parse(Buffer.from(process.env.GOOGLE_FIREBASE_KEY, 'base64').toString()) firebase.initializeApp({ credential: firebase.credential.cert(serviceAccount) })
- Using a
- Code the push notifications. More details under the Code snippets annex section called Push notification in NodeJS.
- Install the Firebase SDK:
npm install firebase
- Add the following property to the
manifest.json
:"gcm_sender_id": "103953800507"
. This number is the same for all mobile apps in the world. - Create a service worker
firebase-messaging-sw.js
(code in the annex firebase-messaging-sw.js) - Register that service worker as soon as you can in your code:
if ("serviceWorker" in navigator) { navigator.serviceWorker .register("../firebase-messaging-sw.js") .then(registration => { console.log(`Registration successful, scope is: ${registration.scope}`); }) .catch(err => { console.log(`Service worker registration failed, error: ${err}`); }); }
- Create a helper to synchronise your logged in user's registration token with your backend (code available in the annex notificationService.js).
- Use the
setUpPushNotification
(from the code above) on each page that need to make sure that push notification are turned on.
TL;DR: Use the Firebase client SDK, but do not use Firebase Authentication. Use its parent Google Cloud Identity Platform instead.
Though Firebase Authentication is a service that can be used and configured to manage your users, do not use it. Instead, use Google Cloud Identity Platform. Identity Platform is the actual underlying service behind Firebase Authentication. The Identity Platform offers more functionalities. IMHO, the most important additional feature is multy-tenancy, which allows to create multiple user pools in the same Google project. Without this feature, all your environments (if you decide to maintain multiple environments in the same project) have to share the same user pool, which is a huge constraints in terms of testing.
Official doc:
- Verifying the ID token server side
- Firebase client User API
- Firebase client Email/Password auth API
- Signing user with Facebook
Using Firebase authentication is quite easy as long as you understand how an OIDC workflow works, and how Firebase authentication is supposed to be used in your app. I won't explain the OIDC workflow here, as this is outside of the scope of this document. Instead, this section will focus on explaining how Firebase authentication is supposed to be used in your app.
- In your GCP project, a new Identity Platform's user pool is configured. This is called
tenant
. That tenant is configured with the various identity provider you wish to support (e.g., email/password, Facebook, Google). - In your app, the
Firebase Client SDK
instantiates a new Firebase client with your tenant's details (i.e.,apiKey
,authDomain
andtenantId
). That client exposes many IdP APIs to sign up and sign in your user. Upon successfull sign in/up, the following a FirebaseUser
object is returned to your client app. That user object should be persisted in the app session because is contains useful pieces of data as well as useful APIs:- User details (e.g., first name, last name, email).
getIdToken
method that can always access an up-to-date Identity Platform user pool'sID token
. This token is a short-lived JWT that proves that your user is successfully authenticated.
- Use that ID token to safely login to your App server. That App server will in turn:
- Verify the ID token with the
Firebase Admin SDK
. This helps to get the
- Verify the ID token with the
Enable Identity Platform
in your Google Project here.
Though it is possible to create a user pool using the default configuration, it is recommended to use tenants instead. Tenants allows to have more than one user pool per project. This is usefull when you wish to host multiple environments (e.g., test, prod) in the same project.
- Enable the multi-tenants feature:
- Click on the
Tenants
in the menu. - Click on
Settings
, select theSecurity
tab and then click on theAllow tenants
button.
- Click on the
- Create a new tenant.
In your client, use one of those two approaches to create the Firebase client:
- Using the CDN, add at the end of the
body
tag:
<!DOCTYPE html>
<html>
<head>
<title>Firebase test</title>
</head>
<body>
<h1>Sign in</h1>
<script src="https://www.gstatic.com/firebasejs/7.18.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.18.0/firebase-auth.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "your-api-key",
authDomain: "your-domain"
}
// Initialize Firebase
firebase.initializeApp(firebaseConfig)
// Sets the tenant
firebase.auth().tenantId = 'your-tenant-id'
const signIn = false
const execAuth = signIn
? firebase.auth().signInWithEmailAndPassword('[email protected]', 'HelloWorld')
: firebase.auth().createUserWithEmailAndPassword('[email protected]', 'HelloWorld')
execAuth
.then(result => {
console.log('HOORAY - WE ARE IN')
// Get the ID token
result.user.getIdToken().then(idToken => {
console.log(idToken)
})
})
.catch(err => {
console.log('OOPS - SOMETHING FAILED')
if (err.code == 'auth/email-already-in-use')
console.log('THAT EMAIL ALREADY EXISTS')
if (err.code == 'auth/user-not-found')
console.log('THAT USER DOES NOT EXIST')
if (err && err.message) {
console.log(err.code)
console.log(err.message)
}
})
</script>
</body>
</html>
- Using NodeJS, after installing the Firebase package with
npm i firebase
:
// Firebase App (the core Firebase SDK) is always required and must be listed first
import * as firebase from 'firebase/app'
// Add the Firebase products that you want to use
import 'firebase/auth'
// The same code as above
This is the part that sucks with Firebase Authentication. You cannot use the ID token to access a protected web server. Instead, the web server must be public and you must manually verify the ID token:
- Get the
service-account.json
that can safely decode the ID token:- Log in to the Firebase account linked to your Google project.
- Click on the cogwheel next to
Project Overview
and then click onProject settings
. - Click on the
Service accounts
tab. - Click on the
Generate new private key
button. - Save that JSON file somewhere safe.
- Configure the Firebase Admin SDK with the JSON file acquired previously as expained in the Step 2 - Setting up the Firebase SDK in your backend section.
- Use the following code snippet to decode the ID token:
The value of the decoded token should be similar to this:
try { const decodedToken = await firebase.auth().verifyIdToken(token) console.log(JSON.stringify(decodedToken, null, ' ')) } catch (err) { console.log(err) }
{ "iss": "https://securetoken.google.com/your-project-id", "aud": "your-project-id", "auth_time": 1597625474, "user_id": "Dew23tmZFGGNbddhkbTYUGU", "sub": "p0TtmZFGGfdsHEKHn4AKmYAk4", "iat": 1597625474, "exp": 1597629074, "email": "[email protected]", "email_verified": false, "firebase": { "identities": { "email": [ "[email protected]" ] }, "sign_in_provider": "password", "tenant": "your-tenant-id" }, "uid": "Dew23tmZFGGNbddhkbTYUGU" }
https://firebase.google.com/docs/auth/admin/custom-claims
https://firebase.googleblog.com/2016/10/authenticate-your-firebase-users-with.html
- Login to your Firebase project.
- Click on the setting cog wheel, and select
Project settings
- Select the
Service accounts
tab. - Click on the
Generate a new private key
button.
Please refer to the Step 2 - Setting up the Firebase SDK in your backend section.
There are no commands to get the piece of information. Instead, the convention is as follow: https://PROJECTID.web.app
When the user is not logged to Firebase yet and runs firebase deploy
, an interactive page opens in the default browser to authenticate the user. In a CI environment, this is not possible. Instead, a token must be acquired and the --non-interactive
flag must be used. To acquire a Firebase token, execute the following command:
firebase login:ci
Once the token is acquired, the deployment command in the CI environment is similar to the following:
firebase deploy --token=$FIREBASE_TOKEN --project $YOUR_PROJECT_ID --non-interactive
To quickly test if your MacOS has been configured to allowed web notifications:
- Open your browser and then open the console in the developer tools.
- Execute this command:
new Notification('Hello')
If a notification appears, then MacOS allows web notitications.
To turn web notifications on/off in MacOS:
- Open the
System Preferences...
. - Select
Notifications
- Select your browser app and there toggle the switch on or off.
Please refer to the Ignoring files section.
NOTE: The following code assumes that a service-account.json credential file has been encoded using base64 into an environment variable called
GOOGLE_FIREBASE_KEY
.
const firebase = require('firebase-admin')
if (!process.env.GOOGLE_FIREBASE_KEY)
throw new Error(`Missing required environment variable GOOGLE_FIREBASE_KEY (required for push notifications)`)
const serviceAccount = JSON.parse(Buffer.from(process.env.GOOGLE_FIREBASE_KEY, 'base64').toString())
// Configure the firebase SDK with the right credentials
firebase.initializeApp({
credential: firebase.credential.cert(serviceAccount)
})
/**
* Pushes a message to one or many client (Android app, iOS app, Web app)
*
* @param {Object} data Message data
* @param {Array} registrationTokens Device tokens. Usually, a token is associated with a specific user for a specific device.
* @return {Void}
*/
const pushToClient = async (data, registrationTokens) => {
if (!data)
throw new Error(`Missing required 'data' argument`)
if (!registrationTokens)
throw new Error(`Missing required 'registrationTokens' argument`)
if (!Array.isArray(registrationTokens))
throw new Error(`Wr0ng argument exception. 'registrationTokens' must be an array`)
if (!registrationTokens[0])
throw new Error(`Wrong argument exception. 'registrationTokens' contain at least one token`)
const message = registrationTokens.length === 1 ? { data, token:registrationTokens[0] } : { data, tokens:registrationTokens }
try {
const response = await registrationTokens.length === 1 ? firebase.messaging().send(message) : firebase.messaging().sendMulticast(message)
if (response && response.failureCount && response.failureCount > 0) {
const failedTokens = response.responses.reduce((acc,res,idx) => {
if (!res.success)
acc.push(registrationTokens[idx])
return acc
},[])
const errorMsg = `Out of ${registrationTokens.length} registration token${registrationTokens.length > 1 ? 's' : ''}, ${response.failureCount} failed: ${failedTokens}`
console.log(`ERROR - ${errorMsg}`)
throw new Error(errorMsg)
}
} catch(err) {
const errorMsg = `Failed to push notification ${JSON.stringify(data)} to registration tokens ${registrationTokens}`
console.log(`ERROR - ${errorMsg}. Details: ${err.stack}`)
throw new Error(errorMsg)
}
}
/**
* Pushes a message to a topic
*
* @param {Object} data Message data
* @param {String} topic
* @return {Void}
*/
const pushToTopic = async (data, topic) => {
if (!data)
throw new Error(`Missing required 'data' argument`)
if (!topic)
throw new Error(`Missing required 'topic' argument`)
try {
await firebase.messaging().send({
data,
topic
})
} catch(err) {
const errorMsg = `Failed to push notification ${JSON.stringify(data)} to topic ${topic}`
console.log(`ERROR - ${errorMsg}. Details: ${err.stack}`)
throw new Error(errorMsg)
}
}
module.exports = {
pushToClient,
pushToTopic
}
importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-messaging.js')
const HOST = 'achiko-wallet'
const APP_URL = 'https://achiko-wallet.web.app'
const firebaseConfig = {
apiKey: "************",
authDomain: "************.firebaseapp.com",
databaseURL: "https://************.firebaseio.com",
projectId: "************",
storageBucket: "************.appspot.com",
messagingSenderId: "************",
appId: "************",
measurementId: "************"
}
// Initialize Firebase
firebase.initializeApp(firebaseConfig)
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(function(payload) {
const promiseChain = clients
.matchAll({
type: "window",
includeUncontrolled: true,
})
.then((windowClients) => {
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i]
windowClient.postMessage(payload)
}
})
.then(() => {
// console.log('SERVICE WORKER: showNotification')
// console.log(JSON.stringify(payload.data))
if (payload && payload.data && typeof(payload.data) === 'object') {
const { title, body } = payload.data
if (title) {
// console.log('Message received in service worker')
return registration.showNotification(title, { body })
}
}
})
return promiseChain
})
// Uncomment this code to do something after the notification has been clicked.
self.addEventListener("notificationclick", event => {
clients.openWindow(APP_URL)
})
import axios from "axios";
import { BASE_API_URL, APP_URL } from "constant";
import * as firebase from "firebase/app";
import "firebase/messaging";
const initializedFirebaseApp = firebase.initializeApp({
apiKey: "**************",
authDomain: "**************",
databaseURL: "**************.firebaseio.com",
projectId: "**************",
storageBucket: "**************.appspot.com",
messagingSenderId: "**************",
appId: "**************",
measurementId: "**************"
})
const messaging = initializedFirebaseApp.messaging()
const refreshPushNotificationToken = async (authToken: string, notificationToken: string) => {
if (!authToken || !notificationToken) {
console.log(
`WARN: Skip refreshing the push notification token. Missing ${
!authToken ? "authToken" : "notificationToken"
}`
);
return;
}
const headers = {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
};
const body = {
deviceId: navigator.userAgent,
token: notificationToken,
};
try {
await axios
.create({
baseURL: BASE_API_URL,
headers,
})
.put("push-notification", body);
// console.log(`Push notification token successfully synched ${notificationToken}`);
} catch (err) {
console.log(`Failed to sync push notification token. ${err}`);
}
};
const setUpPushNotification = async (authToken: string) => {
if (!authToken) {
// console.log(`WARN: Skip refreshing the push notification token. Missing authToken`);
return;
}
await messaging
.requestPermission()
.then(async () => {
const token = await messaging.getToken();
if (token) await refreshPushNotificationToken(authToken, token);
})
.catch(err => {
console.log("Unable to get permission to notify.", err);
});
navigator.serviceWorker.addEventListener("message", message => {
// console.log("CLIENT: Message received");
if (
Notification &&
Notification.permission === "granted" &&
message &&
message.data &&
message.data.data &&
typeof message.data.data === "object"
) {
const { title, body } = message.data.data;
if (title) {
// console.log(`SHOW message now...`);
const notification = new Notification(title, {
body,
icon:
"https://static.wixstatic.com/media/631ee9_28b1b979cb974ef7a1ecc004aac44f70%7Emv2.png/v1/fill/w_32%2Ch_32%2Clg_1%2Cusm_0.66_1.00_0.01/631ee9_28b1b979cb974ef7a1ecc004aac44f70%7Emv2.png",
});
notification.onclick = event => {
event.preventDefault(); // prevent the browser from focusing the Notification's tab
window.open(APP_URL, "_blank");
};
}
}
});
};
export { refreshPushNotificationToken, setUpPushNotification };
- Official Firebase - How to push notifications from the server
- How to add push notifications to a web app with Firebase?
- Medium lifesaver on how to fix registering the freaking service worker
- How to send Notification to React APP using Firebase
- How to toggle/untoggle push notifications on a web app?
- Retrieve Google Access Token after authenticated using Firebase Authentication