Overview
Firebase allows developers to create a fully featured backend on top of servers and APIs operated by Google.
overall benefits
- solid developer experience
- it scales to world-scale use
- generous free-tier and pay-as-you-go pricing
- high quality docs, well supported by AI models
- actively developed and maintained
main backend components covered in this document
- authentication with Firebase Auth
- database with Cloud Firestore
- storage with Cloud Storage
- serverless functions with Cloud Functions
focus of this document: web-centric
We create a backend for web-apps, and use the web-centric client SDKs. We default to TypeScript, and pick Node.js as the runtime for Cloud Functions.
CLI tool
The Firebase CLI tool enables several workflows:
- Emulate the Firebase backend locally, to run it and debug it at no cost and without a deploy step.
- Scaffold the Cloud Functions' directory, and deploy Cloud Functions.
- Submit secrets and API keys to Google, making them available to Cloud Functions.
- Add and deploy security rules.
- List the Firebase projects linked to the Google account.
the CLI executable
The firebase-tools package installs the firebase CLI executable.
npm install -g firebase-tools
firebase
underlying Google account
Firebase projects are linked to a Google account.
firebase login:list # prints current Google account
firebase login
firebase logout
list projects and select one
firebase projects:list
firebase use imagetales
project configuration and scaffolding
The init command enables several workflows. We can:
- scaffold the Cloud Functions directory
- set up and configure emulators
- add security rules for Firestore and Cloud Storage
firebase init
help
- print the list of Firebase commands.
- print the details about a given command.
firebase help
firebase help emulators:start
firebase help deploy
list deployed functions, deploy functions
firebase functions:list
firebase deploy --only functions
firebase deploy --only functions:requestPlanet
manage secrets
firebase functions:secrets:access ABC_API_KEY
firebase functions:secrets:set ABC_API_KEY
firebase functions:secrets:destroy ABC_API_KEY
start and config emulators
firebase emulators:start
firebase emulators:start --import emulator-data --export-on-exit
We set which emulators to run with firebase.json. We scaffold this file with firebase init. If the port is omitted, the emulator uses the default one.
{
"emulators": {
"firestore": { "port": 8080 },
"auth": { "port": 9099 },
"functions": { "port": 5001 },
"storage": { "port": 9199 },
"ui": { "enabled": true }
}
}
manage and deploy security rules
The storage emulator requires storage access rules.
- We define Storage rules in storage.rules.
- We define Firestore rules in firestore.rules
- We refer to the rules in
firebase.json:
{
"storage": { "rules": "storage.rules" },
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}
We deploy the rules:
firebase deploy --only storage
firebase deploy --only firestore:rules
gcloud: Google Cloud CLI tool
gcloud enables some operations not available with the firebase tool, such as listing secrets of a given project or describing a Storage bucket.
We call gcloud from the Google Cloud Console's Cloud Shell (it is pre-installed), or we install it locally from an archive provided by Google.
gcloud secrets list --project <PROJECT_ID>
gcloud storage buckets describe gs://abcd.firebasestorage.app
SDKs
We interact with the backend with the help of SDKs. In this book, we describe the JavaScript SDKs.
client SDKs
The client SDKs run on unprivileged clients, such as browsers. They can also run in Node.js apps that want to run unprivileged.
The client SDKs live in the firebase package:
npm i firebase
admin SDK: privileged environments
The admin SDK is designed to run on secure environments as it relies on privileged service accounts.
The admin SDK authenticates itself against Google servers using a privileged account called a service account. Service accounts are managed by Google, scoped to a Firebase project and have specific entitlements. Such accounts are not subject to security rules and are already authenticated.
As Cloud Functions run on Google servers pre-configured with appropriate service accounts, it is fitting and convenient to use the admin-SDK. The firebase-admin package is designed to run on a Node.js server:
npm i firebase-admin
Cloud Functions SDK
We define Cloud Functions with the (Node.js) Cloud Functions SDK.
We have the package listed as a dependency after scaffolding the Cloud Functions directory with firebase init.
"firebase-functions": "^7.0.0",
Project setup and initialization
identify the Firebase project (client SDK)
The config object stores credentials to identify the Firebase project when interacting with Google servers:
const firebaseConfig = {
apiKey: "....",
authDomain: ".....firebaseapp.com",
projectId: "....",
storageBucket: ".....firebasestorage.app",
messagingSenderId: "....",
appId: "....",
}
register one or more configs
We register the config (client SDK) and keep a reference to the app helper , which we initialize other services with:
const app = initializeApp(firebaseConfig)
When working with several Firebase projects, we get a helper for each. The first helper has a "[DEFAULT]" internal string identifier. We provide string identifiers for the additional project we want to work with.
const app1 = initializeApp(firebaseConfig1)
const app2 = initializeApp(firebaseConfig2, "bar")
When initializing the admin SDK on Google servers that run Cloud Functions, the admin SDK finds the credentials on its own. We initialize it without a config:
const app = initializeApp()
Auth Overview
authenticate app users
The Auth client SDK authenticates users, and notifies the app about Auth events. It provides several authentication flows.
the auth helper and reading currentUser
We keep a reference to the auth helper to read currentUser. We also use it in auth related functions.
const auth = getAuth(app)
auth.currentUser // User | null
currentUser starts as null. When the SDK has finished loading, and given that the user has logged in, currentUser switches to a User instance.
As a User instance, It exposes the user identifier (uid). The other properties are optional:
currentUser.uid
currentUser.email
currentUser.phoneNumber
currentUser.displayName
currentUser.isAnonymous
react to authentication events
We register a callback on onAuthStateChanged, which Firebase runs on auth events. Firebase gives us a user object (of type User | null).
onAuthStateChanged(auth, (user) => {
if (user) {
// user.uid
}
})
Auth events:
-
the auth SDK has finished loading and no user is authenticated
-
the user has registered (sign up)
-
the user has logged in (sign in)
-
the user has logged out (sign out)
Login occurs in three specific scenarios:
- the user fills out the standard login form or logs in through an identity provider (hard-login)
- the user is recognized by the SDK and is logged in automatically (credentials stored in browser)
- (canonically a registration) the user is automatically logged in after a successful sign-up. Note: a single authentication event occurs.
React patterns
We make the authentication status part of the React state. For example, we work with a isSignedIn variable. We make the display of the authenticated area conditional on isSignedIn being true.
On page load, the Auth SDK is loading: It's best to wait for the SDK to load before making any use of isSignedIn, given that the state should be indeterminate.
As such, we track the loading state in a one-off state variable, which becomes true on the first authentication event. Only then do we set isSignedIn with correct state and make use of it.
If we were to read isSignedIn before load, and because we initialize it to false for simplicity, we would have a rapid UI flicker when it switches from false to true if applicable.
const [hasLoaded, setHasLoaded] = useState(false)
const [isSignedIn, setisSignedIn] = useState(false) // the initial state shouldn't be relied upon.
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setHasLoaded(true)
setisSignedIn(Boolean(user)) // accurate state
})
return unsub
}, []) // subscribe once, unsubscribe on unmount.
if (!hasLoaded) return null
if (!isSignedIn) return <Lobby />
return <Ingame />
sign out
sign out is consistent across all authentication flows:
signOut(auth)
Email-Password accounts
A provider that relies on collecting the user's email and password.
registration and hard-login
On success, we receive a credential of type UserCredential.
register:
createUserWithEmailAndPassword(auth, email, password).then((credential) => {
credential.user // User
})
hard login:
signInWithEmailAndPassword(auth, email, password).then((credential) => {
credential.user // User
})
send a password reset email
We ask Firebase to send a password-reset email to the provided email. We can customize the email content through the Firebase console:
sendPasswordResetEmail(auth, email)
email account's providerData (implementation detail)
The user object of type User has a providerData field with password as the providerId value:
{
"providerData": [
{
"providerId": "password",
"uid": "user@example.com",
"email": "user@example.com",
"displayName": null,
"phoneNumber": null,
"photoURL": null
}
]
}
Identity Providers
We allow users to authenticate with external provider accounts, such as with their Google account or Apple account.
select one or several providers
Note: We enable providers in the Firebase console.
const gProvider = new GoogleAuthProvider() // Google account provider
authentication flows
Alternative flows:
- the user authenticates through a popup window.
- the user authenticates through a redirect.
A flow is initiated through a user action, such as tapping a button. Both flows handle sign-in and sign-up through a single, undiscriminated action. As such, we use generic labels, such as:
- Continue with Google
- Authenticate with Google
On success, a flow triggers an authentication event. The flow functions return a credential object (UserCredential), that exposes the user object:
const credential = await signInWithPopup(auth, gProvider)
credential.user // User
Note: We can detect it is a new user through a helper method:
const userInfo = getAdditionalUserInfo(credential)
if (userInfo?.isNewUser) {
}
popup flow specifics
The popup flow may fail if the browser doesn't allow popups.
const credential = await signInWithPopup(auth, gProvider)
redirect flow
The redirect flow relies on navigating to another page and navigating back.
It requires extra work unless the website is hosted on Firebase Hosting.
Anonymous accounts
Register an account with no personal information from the user:
signInAnonymously(auth)
The generated credentials are stored in the browser: users cannot access their account from other devices, and cannot recover it if they lose the credentials (browser wipe).
Auth-triggered Cloud Functions only partially support anonymous accounts:
- The v1 auth functions such as
onCreate()correctly trigger. - The v2 auth blocking functions such as
beforeUserCreated()don't trigger (as of writing)
check if the account is anonymous
On the client, we check the isAnonymous prop from currentUser:
auth.currentUser?.isAnonymous
In auth-triggered Cloud Functions, we read providerData (from the UserRecord which resembles the client SDK's User).
export const onRegisterNonBlocking = auth.user().onCreate(async (userRecord) => {
userRecord.providerData.length === 0 // empty array for anonymous accounts
})
convert to a non-anonymous account (client SDK)
We link the account to another provider. Since the user already exists (auth.currentUser), we provide it to the linking function. We link to an email provider or to an identity provider:
// email
const emailCred = EmailAuthProvider.credential(email, password)
await linkWithCredential(auth.currentUser, emailCred)
// Google account with popup
const gProvider = new GoogleAuthProvider()
await linkWithPopup(auth.currentUser, gProvider)
Manage users
We can manage users in the Firebase console or programmatically via the admin-SDK:
import { getAuth } from "firebase-admin/auth"
const auth = getAuth()
list users
listUsers() fetches at most 1000 users at once. If we have more users, we use pagination.
const result = await auth.listUsers() // implied 1000 max
const users = result.users
users.forEach((user) => {
user // UserRecord
user.uid
user.email
// HTTP-date string (RFC 1123)
user.metadata.creationTime // "Tue, 13 Jun 2023 17:00:00 GMT"
user.metadata.lastSignInTime // "Wed, 14 Jun 2023 17:00:00 GMT"
})
Firestore
conceptual
Firestore is a schema-less database made of collections and documents of loose shape (NoSQL), most similar to MongoDB.
- A collection is a set of documents.
- A document is a set of fields holding primitive data types (number, string, timestamps...) or object types (maps and arrays). A document has up to 20k fields and stores up to 1 MiB of data.
- A reference serves to refer to a collection or to a document. It doesn't guarantee the collection or document's existence in the database: It's merely a path that points to a location.
import paths and documentation
We interact with the database with the client SDK or with the admin SDK. The admin SDK's firestore sub-package is a wrapper around a google-cloud package: It has the same syntax and capabilities.
"firebase/firestore" // client SDK
"firebase/firestore/lite" // client SDK
"firebase-admin/firestore" // admin SDK
helper object
We init a db object, for use in Firestore-related functions.
// const app = initializeApp()
const db = getFirestore(app)
Collection
Collection Reference
use the collection reference
We use a collection reference when an individual document reference is not needed or not yet available:
-
fetch all documents (it acts as a query):
getDocs(colRef) -
build a query (targeting the collection):
query(colRef, filters..) -
build a random-ID document reference:
doc(colRef), or one that refers to a specific document:doc(colRef, docId) -
add a random-ID document to the collection in a single step:
addDoc(colRef, data).
build a collection reference
We use the path that identifies the collection uniquely. Root collections have the simplest path: the collection name, such as "users" (no starting slash). Sub-collections' paths are built from several components.
We set the path as:
-
a string, with slash separators.
-
a sequence of strings, with no slash separators.
const colRef = collection(db, "users")
const colRef = collection(db, `users/${uid}/custom_list`)
const colRef = collection(db, "users", uid, "custom_list")
const colRef = db.collection(`users/${uid}/custom_list`) // admin SDK
TypeScript: a collection's documents as DocumentData
Collections are schema-less: they don't define the shape of their documents.
When receiving data from the database, the client SDK instantiates documents with no regard to the content: documents are of any shape and may differ from one another. It types them as DocumentData, which doesn't provide information about the content.
TypeScript: annotate the document's type at the collection level.
We provide a more precise type at the collection reference level. The simplest way to do it is through a type assertion:
const colRef = collection(db, "players") as CollectionReference<Player, Player>
Converter (optional)
the case for a converter
The SDK supports working with two document shapes on the client:
CollectionReference<AppModel, DbModel>
DbModel represents the object that the SDK instantiates from the raw data. It is DocumentData by default.
We can add a converter to transform it to a different shape for use in the app.
AppModel represents the object after the converter's transformation. It also defaults to DocumentData. We set it to whatever type the converter converts to.
Before sending to Firestore, the converter transforms back AppModel to DbModel.
Transformation examples:
- We transform the DbModel's Timestamp field to an AppModel Date field.
- We add properties to AppModel.
implement the converter
We transform the documents at the app boundaries:
- upon receiving from Firestore (
fromFirestore()) - upon sending to Firestore (
toFirestore())
We define the functions and add them to the converter.
fromFirestore() takes the snapshot as instantiated:
fromFirestore(snapshot: QueryDocumentSnapshot<FirestoreWorkout>): Workout{
// to client shape
// FirestoreWorkout -> Workout
const firestoreItem = snapshot.data()
const workout = { ...firestoreItem, date: firestoreItem.date.toDate()}
return workout
}
toFirestore() takes the object in its AppModel shape.
toFirestore(workout: Workout) {
// to database shape
// Workout -> FirestoreWorkout
return { ...workout, date: Timestamp.fromDate(workout.date)}
}
We gather the transforms in the converter (FirestoreDataConverter). While the type are inferred from the transforms, we can still add them at the converter level:
// FirestoreDataConverter<AppModel, DbModel>
const myConverter: FirestoreDataConverter<Workout, FirestoreWorkout> = {
toFirestore() {},
fromFirestore() {},
}
We attach it to the collection reference to let it type its documents.
const colRef = collection(db, "players").withConverter(conv)
Document
Document reference
The document reference identifies a document within the database, and embeds meta information:
docRef.id // "Nk....WQ"
docRef.path // "users/Nk....WQ"
docRef.parent // colRef
use the document reference
We use the reference for most CRUD operations:
-
read the document:
getDoc -
update an existing document (it errors if the document doesn't exist):
updateDoc -
delete the document:
deleteDoc -
create the document, or override an existing one (upsert):
setDoc
Note: addDoc() works without a document reference since it generates a new one on the fly.
build a document reference
The document's path identifies it uniquely. We set the path as a single string or build it from string components:
const docRef = doc(db, "users", id)
const docRef = doc(db, "users/Nk....WQ")
// const docRef = collectionRef.doc("NkJz11WQ") // admin sdk
Alternatively, we provide the collectionRef and the document ID, or only the collectionRef. In the latter case, the SDK generates a random ID.
const docRef = doc(collectionRef, id)
const docRef = doc(collectionRef) // random ID
read document at reference (get)
The get operation succeeds even when no document exists. We receive a Document snapshot which may be empty.
As such, checking the document existence requires to read inside the Document snapshot.
getDoc(docRef) // DocumentSnapshot
// docRef.get() // DocumentSnapshot
Document snapshot
The Document snapshot is a wrapper that doesn't guarantee the document existence. It exposes the document (or its absence) via a getter. The document's type is DocumentData or the more specific type we provided. It can also be undefined, unless it comes from a query (see Query)
Note: data() is a function because it accepts some configuration.
docSnapshot.exists()
docSnapshot.data() // DocumentData | undefined
It also exposes helpers and metadata.
docSnapshot.id // NkJ...7f
docSnapshot.ref // DocumentReference
docSnapshot.metadata // SnapshotMetadata
Query a specific field
docSnapshot.get("address.zipCode") // low use
real-time listener on a single document
Set up a real-time listener on a (single) document reference:
const unsub = onSnapshot(docRef, (docSnapshot) => {
docSnapshot.data() // DocumentData | undefined
})
Query
overview
A query matches documents based on a set of criteria, and not based on a set of document references.
the result of a query: a query snapshot
The snapshot hosts a list of document snapshots (docs). The list is empty when no match occurs.
Otherwise, it contains only non empty snapshots, of type QueryDocumentSnapshot, which is DocumentSnapshot except that data() cannot be undefined.
querySnapshot.docs // list of document snapshots (QueryDocumentSnapshot)
querySnapshot.empty
docSnapshot.data() // DocumentData (not undefined)
const cats = querySnapshot.docs.map((docSnap) => docSnap.data())
a collection reference is a query
A collection ref can serve as a query, the one that targets all documents (get):
getDocs(colRef)
// getDocs(q)
colRef.get()
// q.get()
build a query
We always provide the collection reference. Then, we can:
- add value-based filters
- set the order
- limit the count
const q = query(colRef, where(..), where(..), orderBy(..), limit(..))
// const q = collection(..).where(..).orderBy(..).limit(..)
where filter: look for documents with a given value
We filter documents based on a value we want to find in a property. We request an exact value or one within a range. Depending on the data, we expect a single match or several.
Note: documents that do not possess the property at all are filtered out.
For example, we look for a document whose id is user.id:
where("id", "==", user.id)
// where(propertyName, operator, value)
We set the requirement for the value: exact match, being different, being smaller or larger, exact match with at least one value, or different from all values.
==
!=
<
<=
>
>=
"in" // the property is equal to either A, B or C
"not-in" // the property is different from A, B and C.
We can also ask the value to be included or excluded from the array if the property is an array.
"array-contains" // the array contains the value
"array-contains-any" // the array contains A, B or C..
order documents based on a field
We order documents based on (the value of) a field, in the ascending or descending order. If omitted, the order defaults to ascending.
orderBy(propertyName, orderDirection)
orderBy("postCount", "asc")
orderBy("postCount", "desc")
We can start from a given value, e.g. documents that have at least (or more than) 10 posts.
startAt(10)
startAfter(10)
pagination: cap the read, then read the next page
Get at most n documents:
limit(20)
To get the next page, we provide a cutoff document (snapshot), stored from the current batch: we then receive the document snapshots that starts after it:
query(colRef, startAfter(docSnapshot), limit(20))
run the query (get)
one time fetch
const qs = getDocs(query)
const qs = query.get()
real-time listener
Set up a real-time listener on the query: we receive a query snapshot:
const unsub = onSnapshot(query, (qs) => {
const documents = qs.docs.map((docSnapshot) => docSnapshot.data())
setMessages(documents)
})
Create and update documents
targeted, strict document creation
On the admin SDK, we can perform strict targeted document creations, aka create a document with a controlled ID and expect a failure if the document already exists:
docRef.create(data)
The client SDK doesn't offer an equivalent function. Instead, we can do a two-step transaction where we read for document existence then write the document conditionally.
random-ID document creation (add)
The client SDK prefers random-ID creation that always succeed because the document won't already exist by design (add):
addDoc(collectionRef, data)
// db.collection("message").add(data)
upsert (set)
An upsert works regardless if a document exists or not. It is destructive, as it override any existing document.
setDoc(docRef, data)
// docRef.set(data)
update documents
We assume the document already exists: we use the update pattern or the set with merge pattern.
The update function is a strict update: it correctly fails if the document doesn't exist.
Both update and set merge expect a change object. We type the change as a Partial or as a Pick of the document:
const change: Partial<User> = { displayName: "Johnny Appleseed" }
updateDoc(docRef, change)
// docRef.update(change)
update uses the provided fields to replace the existing ones, the other fields being left unchanged.
To mutate a single field in an object field, we target it with dot notation. If we target the object instead, the omitted sub-fields are deleted (different from set with merge)
// sub-field
const change: Partial<User> = { "address.city": "Lyon" }
updateDoc(docRef, change)
If TypeScript complains about the dot notation, we use the FieldPath overload:
updateDoc(docRef, new FieldPath("address", "city"), "Lyon")
partial update with set and merge
the merge option changes the meaning of set: we are now providing a change object, not the new object.
What we target in the change object is what gets changed, including sub-fields. The omitted sub-fields are preserved (deep merge): there is no need for dot notation:
const change = { address: { city: "Lyon" } } // only changes city in address
setDoc(docRef, change, { merge: true })
// docRef.set(data, { merge: true })
relative change (increment, decrement)
We ask the server to change the field by n, which can be positive or negative. The current value is unknown:
const change = {
activityScore: increment(1), // or e.g. -1
}
// docRef.update({
// count: FieldValue.increment(1),
// })
delete field
We ask the server to delete a field. This shortcuts the need to fetch the document first and store it second omitting the given field:
updateDoc(docRef, {
fleet: deleteField(),
})
// docRef.update({
// fleet: FieldValue.delete(),
// })
server timestamp field
Ask the server to generate a Firestore timestamp value.
updateDoc(docRef, {
count: serverTimestamp(),
})
// docRef.update({
// count: FieldValue.serverTimestamp(),
// })
delete document
deleteDoc(docRef)
// docRef.delete()
Batch writes
We gather writes in a batch object and ask Firebase to perform (commit) all the writes at once.
The operation is atomic: if one write fails, the others fail as well. This prevents a broken state where only some documents are updated.
A single network request is sent.
Note: the functions are namespaced on batch both for the client SDK and the admin SDK.
batch changes then commit
Collect up to 500 writes in a batch object, and execute them with commit().
In this example, we use update(), but we could also use set() or other methods.
const batch = writeBatch(db)
// const batch = db.batch() // admin SDK
const change = { timezone: "Europe/London" }
batch.update(docRef1, change)
batch.update(docRef2, change)
// ..
// ..
await batch.commit()
other batch operations
batch.set(docRef, data)
batch.set(docRef, change, { merge: true })
batch.update(docRef, change)
batch.delete(docRef)
batch.create(docRef, data) // Admin SDK
Transactions
Read and write atomically with transactions
conceptual
Transactions are for operations where a preliminary read is required to determine the applicability, nature and extent of a tentative write.
A naive read and write is subject to desync: by the time the write order hits the database, the motivating condition may have changed: the data can change between the database read and the database write, as simple reads have no lock effect.
A transaction is a guarantee that by the time the database commits the write, the data hasn't changed since the initial read, so that the check that was performed on the data is still relevant.
For example, if credits is positive and sufficient, the condition is fulfilled, but by the time we are about to commit the purchase, we want credits not to have changed since the read.
implementation
The simplest way to guarantee it is to lock the document between the read and the write. The Admin SDK locks the document during the read to write time-window.
The client SDK doesn't lock the document, because the time between the read and write orders can become overly long and degrade the UX for the rest of users. Instead:
- the SDK is aware of the document's version (by the time it was read), and asks the database to only perform the write if the document is still of this version (possibly tracked with an updateTime flag or equivalent).
- The database allows changes initiated by other operations, if any.
- On receiving the conditional write order, the database enforces the condition: it proceeds only if the document hasn't changed. Otherwise, it rejects the transaction: it is up to the client SDK to attempt a new transaction. The client SDK does retry by default, up to 5 total attempts. (retry strategy).
This pattern is called emulated optimistic concurrency.
See also: Transaction serializability and isolation (Firestore).
the runTransaction function
runTransaction expects a callback. transaction is a helper that holds the read and write methods (get, update, set).
Note that we await reads, but don't await writes, since writes are grouped up on the transaction object and sent all at once.
In case of failed preconditions, we abort the transaction with a throw:
await runTransaction(db, async (transaction) => {
// read
const snapshot = await transaction.get(docRef)
// check condition
if (!snapshot.data()) throw Error("No such event!")
const count = snapshot.data().count
if (count >= 10) throw Error("Sorry, event is full!") // Abort
// proceed
transaction.update(docRef, { count: count + 1 })
})
// admin SDK
// await db.runTransaction(async (transaction) => {
// identical API
// })
Timestamp value type (advanced)
Note: storing dates as ISO strings is simpler and more portable.
As the Firestore database comes with a native value type for storing dates called timestamp, we describe using this pattern in this article. The Firestore SDK comes with a Timestamp type that represents a timestamp field.
storing timestamps
As we attempt to store data, the SDK detects Date and Timestamp fields and assumes we want to store them as timestamps.
const user = {
createdAt: new Date(),
createdAt_: Timestamp.now(),
}
When preparing data for the HTTP request, the SDK serializes Date objects and Timestamp objects to plain objects with a single timestampValue property:
{
"createdAt": { "timestampValue": "2025-10-07T18:47:13.279000000Z" },
"createdAt_": { "timestampValue": "2025-10-07T18:47:13.279000000Z" }
},
The database detects this pattern and sets the fields as timestamps.
receiving timestamps
The SDK's Timestamp type represents database timestamps. As we receive timestamp fields from the database, the Firestore SDK instantiates them as Timestamp objects.
Firestore Security rules
We define the security rules in the Firebase console or in a firestore.rules file, referenced by firebase.json. Firebase doesn't bill reads and writes denied by security rules.
rules version
rules_version = "2"
firestore scope
We start by scoping the rules to cloud.firestore
service cloud.firestore {
// ...
}
database scope
We scope the rules to the current database. The security rules only affect the current database, so this can be seen as superfluous. But we use the wildcard when querying separate documents (see below).
match /databases/{database}/documents {
// ...
}
set rules for a given collection
We target a collection. The document ID wildcard holds the requested document's ID. We can name it with what the document represents:
match /users/{user_id} { // document ID wildcard
// ...
}
operations and condition
allow operation, operation: if condition;
operations
read
create
update
delete
authentication, user ID
If the user is not authenticated, request.auth is null. We can filter out unauthenticated users:
allow read: if request.auth != null;
The user's authentication uid (if logged in) is available as request.auth.uid:
request.auth.uid
Note: if auth is null, trying to read uid triggers a failsafe mechanism that denies the request.
authorize specific documents
Document ID based authorization: We authorize the operation if the document's ID matches some condition:
match /players/{player_id} {
allow read: if request.auth.uid == player_id;
}
Field value based authorization: we authorize the operation based on the value of a specific field. Either from the requested document or from the uploaded document.
- resource.data represents the requested document.
- request.resource.data represents the uploaded document
- For example, we check the requested document's owner property against auth.uid. In this case, we ignore the uploaded document's data which can be tampered with.
match /planets/{planet_id} {
allow read: if request.auth.uid == resource.data.owner.id;
}
If the document is missing the field, the request is denied.
authorization based on separate documents
We read a document with get(). It is a billed read.
first example (do not use)
This unlocks a pattern where we read authorization data in a separate document, such as in the user document, which would store the user's entitlements:
get(/databases/$(database) / documents / users / $(request.auth.uid)).data.rank
For example, we read the user's rank in the database to authorize a write on any character:
match /characters/{character_id} {
allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.rank == "Game Master";
}
Note: We should use Firebase Auth custom claims instead:
request.auth.token.rank == "Game Master";
second example
For example, we read the player's character's zone to determine if it can read the requested overworld character:
match /overworld_characters/{overworld_character} {
allow read: if get(/databases/$(database)/documents/characters/$(request.auth.uid)).data.zone == resource.data.zone;
}
Note:
- if a query matches 10 documents, the get() is run only once, so it only triggers a single read
- if a query, by its nature, could match at least one document that is not authorized by the security rule, the entire query is rejected. This forces the client to query only authorized documents. In the example, it forces the client to use a where() clause instead of fetching all overworld_characters. That is, we cannot rely on security rules to filter documents out of a broad query.
payload validation
request.resource.data is the request's payload. We can validate critical fields:
// simple check
request.resource.data.age >= 0
// check against auth uid.
request.auth.uid == request.resource.data.uid;
// check both requested document and uploaded document
allow update,delete: if
request.auth.uid == resource.data.uid
&&
request.auth.uid == request.resource.data.uid;
Alternative: We can perform validation in Cloud Functions and forbid writes coming from the client.
Storage
reference
file terminology
Firebase Storage is a wrapper around Google's Cloud Storage, a cloud storage service. It is technically an object storage service because it stores immutable objects in a flat bucket, instead of files in a hierarchical filesystem.
Firebase Storage reintroduces the concept of files, folders and file hierarchy, and uses that terminology exclusively. Among other things, It does so by naming objects with their hierarchical path such as public/abc.png.
project's default bucket (implementation detail)
A Firebase project is given a default bucket, with a given URI:
"gs://<PROJECT-ID>.firebasestorage.app"
"gs://<PROJECT-ID>.appspot.com" // old default bucket URIs
The bucket's URI serves to distinguish it from other buckets.
- It is made of two components: a
gs://prefix and a domain name. - The default bucket's domain uses the project's name as a subdomain, which makes it globally unique.
- If we add another bucket, we pick a globally unique name by ourselves:
"gs://<GLOBALLY-UNIQUE-ID>" // non-default bucket URI
The URIs only act as identifiers. There is no matching HTTP endpoint and no server listening to, for example, abc.firebasestorage.app.
storage helper
We get a storage helper for use in various storage related functions:
- We don't name it storage because Firebase already exports a storage variable. We use another name such as storageService or bucket.
- The client SDK uses the default bucket unless we specify another one in the initializer:
const storageService = getStorage(app)
const storageService = getStorage(app, "gs://...")
// const bucket = getStorage().bucket(); // admin SDK
File references and metadata
file path
A file path uniquely identifies a file in the bucket. It includes the file extension. It starts from the bucket's root.
file reference
- We use references to interact with files
- A file reference does not guarantee the file existence.
- We build them with file (full) paths:
const fileRef = ref(storage, "tts/2F14Izjv.mp3")
// const fileRef = bucket.file("tts/2F14Izjv.mp3") // admin SDK
Note: ref can also build folder references.
The properties are of limited use:
ref.bucket // "abc.firebasestorage.app"
ref.fullPath // "tts/abc.mp3"
ref.name // "abc.mp3"
// computed references
ref.parent // ref(storage, "tts")
ref.root // ref(storage, "/")
file metadata
A file metadata, of type FullMetadata, or FileMetadata on the admin SDK, contains various information about the file:
metadata.size // 1048576 (bytes)
metadata.contentType // "audio/mpeg" (MIME type)
metadata.timeCreated // "2026-01-04T12:34:56.789Z"
metadata.ref // file reference
// repeat from fileRef
// metadata.bucket
// metadata.fullPath
// metadata.name
We fetch an existing file's metadata:
const metadata = await getMetadata(fileRef)
// admin SDK
// const [metadata] = await fileRef.getMetadata()
List files and folders
folders and prefix terminology
The API describes folders as prefixes, but the docs also mention folders.
folder existence
A file, by its name alone, can create one or more nested folders. It occurs when its name contains subpaths. For example, abc/def/hello.pdf creates two folders: abc and def. Those folders are an artificial byproduct.
By design, those folders can't be empty, because they derive from a nested file.
use a folder reference to list its content
We build a folder reference to list its content. It outputs a shallow list: we see the top level files and folders.
The list discriminates files (items) from folders (prefixes), putting them into separate arrays, but both arrays are typed with the same StorageReference element type.
Note: list() is a capped version that expects a count limit.
folderRef = ref(storage, "uploads")
const result = await listAll(folderRef)
const result = await list(folderRef, { maxResults: 100 })
result.items // StorageReference[]
result.prefixes // StorageReference[]
Read, download files
general considerations
-
The client SDK's functions are subject to access-control rules.
- Some functions allow the user, after access-control, to save a bearer URL which is not subject to security rules. This pattern is a one-off access control.
-
Download workflows are influenced by the browser restrictions regarding the file's URL.
get a HTTP URL on the client
We request a read URL. Access control is performed during such request.
The returned URL is a bearer URL, which is not subject to access-control. We consume it as a regular URL, outside the realm and control of the Storage SDK.
Note: the URL remains valid unless manually revoked at the file level in the Firebase Console, or with the admin SDK.
getDownloadURL(fileRef).then(url => ...)
consume a cross-origin HTTP URL on the client (browser-specific)
The bucket's file lives on a separate origin than the origin that ships the web-app. For the browser, the file's URL is cross-origin. The challenges and patterns to consume a cross-origin URL are not specific to Firebase.
The way we consume the URL determines if CORS headers are necessary:
- The browser allows cross-origin URLs in media elements' src attribute (hot linking), with no CORS header requirement.
- The browser allows navigating to cross-origin URLs (basic browser behavior). For example, we navigate to an image in a new tab.
- The browser doesn't allow background fetch of cross-origin resources unless explicit CORS headers are present on the server. This applies to fetch() and functions that rely on it.
Buckets do not have permissive CORS headers by default, but we can add them on demand. CORS headers whitelist one, several or all domains. We use gcloud to whitelist our domain (see the dedicated CORS chapter).
download a Blob with the client SDK
A blob is an opaque object that we fetch and transform to a local URL for easier saving. When using getBlob():
- access rules are enforced
- CORS headers are required (it uses fetch() under the hood)
We create a local (same-origin) URL out of the blob, to avoid the browser restrictions against cross-origin URLs. It restores the ability to download content through a single click, without navigating to a different URL (see below).
getBlob(fileRef).then((blob) => {
// create a local URL and trigger download imperatively
})
add the download attribute to an anchor tag with a local URL
The download attribute on anchor tags (<a href="" download>) offers one-click downloads for same-origin URLs or local URLs.
For cross-origin URLs, clicking the anchor tag triggers standard browser navigation instead: the browser navigates to the resource and shows its full URL.
create a local URL out of a blob (browser specific)
This example also triggers download programmatically, and revokes the local URL for clean up. We set the user-facing file name by setting the download attribute:
// 3. Create a local URL out of the blob
const objectURL = URL.createObjectURL(blob)
// 4. Use the local URL and trigger the download
const link = document.createElement("a")
link.href = objectURL
link.download = img.id + ".png"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 5. Clean up by revoking the local URL
URL.revokeObjectURL(objectURL)
Upload data
client SDK
upload a Blob or a File
We prepare some data in a JavaScript Blob or File object, and upload it to the reference:
const result = await uploadBytes(fileRef, file) // File object
- The upload is a non-conditional upsert which overrides existing files.
- It makes the file immediately downloadable through the SDK read functions.
- On success, we receive an UploadResult, which wraps the bucket file's metadata and the file reference.
result.metadata // FullMetadata
result.ref
(advanced) upload and track the progress
For each tick, we receive a snapshot. We may show the upload progress.
const uploadTask = uploadBytesResumable(ref, file)
uploadTask.on(
"state_changed",
/* on snapshot */
function (snapshot) {
// snapshot.bytesTransferred
// snapshot.totalBytes
// snapshot.state // "paused" | "running"
},
function (error) {},
function () {
/* on completion */
getDownloadURL(uploadTask.snapshot.ref).then(/**/)
},
)
admin SDK
upload a Node.js Buffer
We prepare some data in a Node.js Buffer, and upload it to the reference.
await fileRef.save(imageBuffer, {
resumable: false,
metadata: {
contentType: `image/png`,
cacheControl: "public, max-age=31536000, immutable",
},
})
Note: it doesn't make the file downloadable for clients: a client getDownloadURL() fails. This is because the underlying Firebase-specific download token in the GC storage object's metadata is missing.
make it downloadable
To make it downloadable for clients, we then use the admin SDK's getDownloadURL(). It adds a permanent download token to the underlying GC storage. It also returns the bearer URL (tokenized URL that embeds this very access token, and is not subject to security rules).
We can store it in a database, return it to the client, or discard it and let the client SDK generates the URL on its own with getDownloadURL() (since it is now downloadable).
const url = await getDownloadURL(fileRef)
We can invalidate the access token from the Firebase console. It makes the file non-downloadable. The bearer URL becomes invalid.
advanced: read and write the token
The token, if present, is in the File's metadata field. We should avoid setting this field manually when using save(). We use getDownloadURL instead (see full example below).
metadata: {
firebaseStorageDownloadTokens: token
}
We can revoke the token:
await file.setMetadata({
metadata: {
firebaseStorageDownloadTokens: null,
},
})
Or rotate the token
await file.setMetadata({
metadata: {
firebaseStorageDownloadTokens: randomUUID(),
},
})
upload image example (admin SDK)
We upload an image and make it readable by clients. We may store the bypass URL.
// 1.0 create a file reference
const fileRef = bucket.file(`generated/${userID}/cat.png`)
// 1.1 create a Buffer object
const imageBuffer = base64ToBuffer(base64Data)
// 1.2 upload the Buffer object
await fileRef.save(imageBuffer, {
resumable: false,
metadata: {
contentType: `image/png`,
cacheControl: "public, max-age=31536000, immutable",
},
})
// 1.3 make it readable by client SDKs (generate a token).
const url = await getDownloadURL(fileRef)
// 1.4 store the bypass URL (if applicable)
// ...
Setting the bucket CORS header
Some read operations require the web client's domain to be whitelisted, through a bucket-side CORS header.
read operations that require a CORS whitelist
Browser reads relying on background fetch rather than navigating to the URL require a CORS whitelist:
- getBlob(fileRef) to get a Blob, which uses fetch() under the hood.
- getBytes(fileRef) to get an ArrayBuffer, which uses fetch() under the hood.
- using fetch() manually with a bearer (tokenized) URL.
white-listing the domain
We add authorized domains to cors.json and send it to Google through the CLI:
cors.json
[
{
"origin": ["https://imagetales.io", "http://localhost:5173"],
"method": ["GET"],
"maxAgeSeconds": 3600
}
]
Register cors.json:
gcloud storage buckets update gs://abc.firebasestorage.app --cors-file=cors.json
For debugging, we can get a description of the existing bucket CORS config:
gcloud storage buckets describe gs://abc.firebasestorage.app --format="default(cors_config)"
Cloud Functions
Cloud Functions are serverless functions: we run code on servers operated by Google.
As it is a secure environment, we can run sensitive tasks: authenticate requests, perform server-side validation, use API keys, make sensitive database writes, and more.
Functions trigger on direct requests, or on events happening in the Firebase ecosystem, such as the registration of new users through Firebase Auth. They can also be triggered by a Cron job.
trigger on direct requests: two options
The first option is to configure and establish a bare-bones REST-API endpoint, called a HTTP function. We configure the endpoint with an Express.js like API.
import { onRequest } from "firebase-functions/https"
The second option is to define a Callable function, a pattern that involves both a server SDK and a client SDK, which together do some heavy lifting on behalf of the developer, such as managing authentication, defining the type of the request's data and response, and offering streaming responses.
import { onCall } from "firebase-functions/https"
select and deploy functions
The main file determines which functions are deployed by default: all the ones that it exports. We determine the main file in package.json. Since it must be a JavaScript file, we refer to the TypeScript compiler output file:
{
"main": "lib/index.js"
}
It is usually a barrel file that re-exports functions from their own files:
export { requestPlayer } from "./requestPlayer.js"
We deploy functions imperatively, all of them or some of them:
firebase deploy --only functions
firebase deploy --only functions:FOO
firebase deploy --only functions:FOO,functions:BAR
We can delete a function imperatively:
firebase functions:delete FOO
configure TypeScript
We use a workflow that transpiles to JS since the functions are deployed as JavaScript functions.
The convention is to store TypeScript code in src/ and transpile towards lib/. The main file is lib/index.js.
tsconfig.json configures the transpilation, targeting the Node.js runtime:
{
"compilerOptions": {
"module": "NodeNext",
"esModuleInterop": true,
"moduleResolution": "nodenext",
"noImplicitReturns": true,
"outDir": "./lib",
"rootDir": "./src",
"sourceMap": true,
"target": "es2022",
"skipLibCheck": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"verbatimModuleSyntax": true
},
"compileOnSave": true,
"include": ["src"]
}
use of the admin SDK
The use of the admin SDK is a natural fit from Cloud functions.
Define Callable functions
The code we run in Callable functions has access to the user's authentication status along with the request's data.
Callable functions support streaming responses: we describe the pattern in a dedicated section.
Overview and syntax
synopsis
onCall<ReqData, Promise<ResData>>(options, callback)
onCall<ReqData, Promise<ResData>>(callback)
the callback
The callback has access to the request object (CallableRequest), which exposes auth and data.
We define the callback async so it returns a promise. The connection is kept open until the promise settles.
;async (req) => {}
;async ({ auth, data }) => {}
- auth is undefined when the request is unauthenticated. It has uid otherwise.
- ReqData defines the data sent by clients.
- ResData defines what the callback returns.
onCall<ReqData, Promise<ResData>>(async (req) => {
req.auth // AuthData | undefined
req.auth?.uid
req.data // ReqData
return { message: "" } // ResData
})
add options
The options object, of type CallableOptions, a subclass of GlobalOptions, sets the region, the secrets, and the number of instances and the concurrency for each instance:
const options: CallableOptions = {
concurrency: 1,
minInstances: 1,
maxInstances: 1,
region: "europe-west1",
}
An instance can handle several requests at once. By default, Firebase runs with no minimum and no maximum.
- Since there are no minimum (minInstances defaults to 0), Firebase doesn't run any instance when the endpoint is inactive. When a request is received, it spins up an instance, which requires some time (cold start), and keeps it alive for a while.
- To avoid cold starts, we can set minInstance to 1, which keeps the instance warm at all time but costs much more.
- We can limit maxInstances to 1 if we prefer throttling traffic instead of scaling up.
concurrency sets how many requests a single instance processes in parallel. Since it can process multiple requests in parallel by default, We only set it to 1 if we want a clean instance for each request or, if we also limit maxInstances to 1, if we want to process one request at a time.
Streaming version
Streaming the response means to send small chunks of data with sendChunk().
The third type argument (StreamData) defines what kind of chunk we stream. We usually stream string chunks.
The request exposes acceptsStreaming, which we read to check if the client supports streaming. When it does, the callback has access to an extra response argument, on which we call sendChunk().
onCall<T, U, V>(options, callback) // streaming Callable
onCall<ReqData, Promise<ResData>, StreamData>(async (request, response) => {
if (response.acceptsStreaming) {
response?.sendChunk("abc") // StreamData
response?.sendChunk("def")
} else return { message: ".." } // fallback
})
Patterns
halt and send an error immediately
We throw an HttpsError with a specific error code which conforms to a predefined list. It defaults to internal error if omitted.
throw new HttpsError("unauthenticated", "unauthenticated")
logger
logger.debug("")
logger.info("")
logger.warn("")
logger.error("")
Callable v1 (deprecated)
define the function
functions.https.onCall(async (data, context) => {
const auth = context.auth
const message = data.message
return { message: ".." }
})
the context object
The context object provides the authentication details, if any, such as the email, and the request metadata such as the IP address, or the raw HTTP request. It is of type CallableContext
check authentication
if (!context.auth) {
throw functions.https.HttpsError("unauthenticated", "you must be authenticated")
}
Invoke Callable functions
On the client, we invoke callable functions through specific handlers provided by the SDK.
configure a functions helper with the firebase project and the region
- Since a client can interact with separate Firebase projects, we specify the project we target. We do so by providing the app helper, which already identifies the project.
- Since Cloud function instances are region-specific, we specify which instance region we target. If omitted, the client SDK targets
us-central1, which errors if no instance runs there:
const functions = getFunctions(app, "europe-west1")
get a handle over the Callable function
We provide the function's name and the type arguments:
const requestPokemonCF = httpsCallable<ReqData, ResData>(functions, "requestPokemon")
invoke and handle the result
- The request data, if any, is of type
ReqDatain our example. - The returned value is of type
HttpsCallableResult<ResData>, which is a container over the actual data, found in the data property, of type ResData in our example:
const result = await requestPokemonCF({ number: 151 })
result.data // ResData
HTTP functions
overview
Configure and establish a bare-bones REST-API endpoint, called an HTTP function. We use an Express.js like API. We respond with JSON, HTML, or plain text:
export const sayHello = onRequest((req, res) => {
res.send("Hello from Firebase!")
})
add options
const options: HttpsOptions = {
region: "europe-west1",
cors: true,
}
export const sayHello = onRequest(options, (req, res) => {})
ExpressJS concepts and syntax
req and res objects have the shape of expressJS req and res objects. We can add middleware.
call the endpoint: standard HTTP request (not Firebase specific)
We read the function's URL at deploy time.
We consume endpoints like regular REST API endpoints. The deployed endpoint URLs look like this:
https://requestPlanet-x82jak2-ew.a.run.app
Run functions on Auth events
Register functions that react to Auth events. Blocking functions can deny the registration of a user. Non blocking functions run after the authentication event has occurred.
Blocking functions
run a function before the user is added to Firebase Auth
We perform validation, and, if applicable, deny the registration by throwing an error. Firebase Auth aborts user creation on throw. The Auth client SDK receives such error and can display it to the user.
export const onRegisterBlocking = beforeUserCreated(options, async (event) => {
const userRecord = event.data // AuthUserRecord === UserRecord
// userRecord.uid
// userRecord.email
// 1.0 validate
if (userRecord?.email?.includes("@hotmail.com")) {
throw new HttpsError("invalid-argument", "don't use hotmail")
}
// 2.0 perform block side effects
// e.g. add a user document to the database:
await addUserDocFor(userRecord)
return
})
Non blocking functions
The non blocking functions run after a user has been created (or deleted) by Firebase Auth.
As of writing, there is no v2 version for the non blocking functions:
auth.user().onCreate(async (user) => {})
auth.user().onDelete(async (user) => {})
example: add the user to the Firestore database (or delete it)
We read the auth user's uid and act accordingly:
export const onRegisterNonBlocking = region("europe-west1")
.auth.user()
.onCreate(async (user) => {
const { uid, email } = user
// add to db
await db.doc("users/" + uid).set({ uid, email })
// delete from db
await db.doc("users/" + uid).delete()
})
on Firestore and Storage events
on Firestore events
Run Cloud functions on database events. They are non-blocking: they run after writes. As they don't prevent writes, we don't call it validation. Instead we use the term sanitization.
sanitize data post-write
List of Cloud Firestore triggers.
For example, onDocumentWritten triggers on:
- onDocumentCreated
- onDocumentUpdated
- onDocumentDeleted
export const onUserWritten = onDocumentWritten("users/{docId}", (event) => {
const docId = event.params.docId // string
const change = event.data
// Change<QueryDocumentSnapshot | DocumentSnapshot> | undefined
const before = change?.before.data() // DocumentData | undefined
const after = change?.after.data() // DocumentData | undefined
})
on Storage events
sanitize data post-upload
the user uploads a file to Firebase Storage. Sanitize data post-upload. For example:
import { onObjectFinalized } from "firebase-functions/v2/storage"
export const generateThumbnail = onObjectFinalized(async (event) => {
const file = event.data
// const fileBucket = file.bucket;
// const filePath = file.name;
// const contentType = file.contentType;
// const metageneration = file.metageneration;
// // Number of times metadata has been generated. New objects have a value of 1.
})
Dates and Timestamps serialization
ISO strings are the better choice
When interacting with Callable Functions, it's best to represent dates as ISO strings. The value and the type stay consistent when sending and receiving on the client and on Cloud functions.
In this article, we explain what happens when we send Date and Timestamp objects to Callable Functions or when we receive them from Callable Functions. Before being sent, both Date and Timestamp are serialized to JSON.
sending Date and Timestamp to Callable Functions (do not use)
Timestamp is a Firestore specific type and doesn't get a special treatment: it serializes to an object with seconds and nanoseconds (through toJSON()).
// on the Cloud function receiving end
type T = { seconds: number; nanoseconds: number }
Date fields serialize to an ISO string (through toJSON()):
date: "2023-10-08T07:54:47.527Z"
When receiving in Cloud functions, we have a type difference between RequestData on the client and RequestData in Cloud functions.
We could manually instantiate Timestamp or Date instances, but it requires manual processing:
new Date(date) // build a Date
new Timestamp(timestamp.seconds, timestamp.nanoseconds) // build a Timestamp
returning data from Callable functions
If we attempt to return a Date object, it serializes to an ISO string as well, through the same mechanism.
If we attempt to return a Timestamp object, it serializes to the internal representation specific to the admin SDK, which is different than the client SDK's Timestamp representation:
possible an object with _seconds and _nanoseconds. We should avoid this pattern:
// on the client SDK receiving end
type T = { _seconds: number; _nanoseconds: number }
Environment variables
Firebase provides a way to register and manage secrets through the CLI. We then read them from Cloud functions, both from deployed functions and from functions running locally in the emulator.
As such, we don't need to store secrets in local .env files.
Manage secrets
set, read and destroy secrets through the CLI
firebase functions:secrets:set ABC_API_KEY
firebase functions:secrets:access ABC_API_KEY
firebase functions:secrets:destroy ABC_API_KEY
gcloud secrets list --project <PROJECT_ID>
Note: the secrets are available both locally (function emulator) and for deployed functions.
read secrets from cloud functions
We declare the secrets requirements in the function options. We then read them from process.env:
const options: CallableOptions = {
// ..
secrets: ["ABC_API_KEY"],
}
onCall<ReqData, Promise<ResData>>(options, async (request) => {
const abcKey = process.env.ABC_API_KEY
})
Https functions:
const options: HttpsOptions = {
// ..
secrets: ["ABC_API_KEY"],
}
onRequest(options, (req, res) => {
const abcKey = process.env.ABC_API_KEY
})
.env file pattern
The .env file pattern should be only used if the CLI pattern is not available or not applicable. We set the env variables in a .env file:
ABC_API_KEY=xxx
The key is then made available to cloud functions the same way as seen above, at process.env, both locally and for deployed functions, as long as the key is listed in the options.
On deploy, the .env file is automatically detected and deployed along functions. See env-variables docs
Debug Functions locally
start the functions emulator
We run the functions with other emulators, or on their own:
firebase emulators:start
firebase emulators:start --import emulator-data --export-on-exit
firebase emulators:start --only functions
npm run serve
invoke callable functions through the SDK
We redirect client invocations towards the emulated functions, but only on localhost:
if (location.hostname === "localhost") {
// ...
connectFunctionsEmulator(functions, "localhost", 5001)
}
invoke callable functions outside the client SDK
We can invoke callable functions outside the client SDK as long as the function emulator is running.
The emulator exposes them on specific URLs. We get the URLs when starting the emulators:
http://localhost:5001/imagetale/europe-west1/get_images
Callable functions can be invoked:
- directly, with a local HTTP request
- through the Firebase REPL shell.
In both cases, the request's data must be present in the data key of the request's body (JSON). The data key must exist.
Note: We turn off authentication requirements when calling callables outside the client SDK
calling functions with curl
We can invoke callable functions with curl:
curl -H "Content-Type: application/json" \
-d '{ "data": { } }' \
http://localhost:5001/imagetale/europe-west1/get_images
calling functions with the Firebase REPL
functions:shell starts the functions emulator and an interactive CLI shell from which we invoke callable functions.
firebase functions:shell
We provide the data key and its content, which represents the request's data:
generateExample({ data: { word: "你好" } })
Schedule Cron jobs
periodic code execution: schedule a Cron job
We schedule a Cron job, with the help of Google Cloud's Cloud Scheduler. We register a job with onSchedule. We set:
- the periodicity, using strings such as
every day 00:00orevery 8 hours. - the timezone (IANA string), the region and the callback function:
export const updateRankingsCRON = onSchedule(
{
schedule: "every day 00:00",
timeZone: "Europe/London",
region: "europe-west1",
},
async () => {
// ...
},
)
The older API (v1) relied on pubsub instead:
export const updateRankingsCRON = functions.pubsub
.schedule("every 8 hours")
.timeZone("Europe/London")
.onRun(async (context) => {
// ..
})