When a user logs in to most Firebase apps, the first thing your app needs to do is create a user document. There are ways to do this automatically, so your app can always function without bugs.
TL;DR#
You can create a user
document automatically when a user signs up by using a Trigger Function, creating a user on the server, or listing for the user login, and then creating it. If you donāt use a server to handle user authentication, you should check for a user document on all sign-ins and create a user document if necessary.
Creating a User Collection#
Once your app needs some features, you will inevitably need to store the user data in a users
collection. This should be done on signUp
and verified on signIn
methods.
User Rules#
Ensure the correct Firestore Rules are in place to verify writability in the users
collection. You could write this a million different ways.
match /users/{document} {
allow write: if requestUser() == requestDoc();
allow read;
}
function requestDoc() {
// 4th path is doc id
return request.path[4];
}
function requestUser() {
return request.auth.uid;
}
š« Bad Method#
This is the method that may come to mind for some of you.
const credential = await signInWithPopup(auth, provider);
const additionalInfo = getAdditionalUserInfo(credential);
if (additionalInfo?.isNewUser) {
await setDoc(doc(db, 'users', user.uid), {
displayName: user.displayName,
photoURL: user.photoURL,
email: user.email,
createdAt: serverTimestamp()
});
}
The problem with this method is that there are some edge cases where clients fail, and a user needs to be created on the first sign-in. Then your app fails when the isNewUser
flag is not set. If you want to do everything on the client, you should always check for a user document (you need to get it anyway) and create one if is not there.
Client Method#
First, we need a function to create a user document. We store the name, photo, and email here in Firestore, but this could be any database. Firebase Authentication is built to work with any database you want.
const createUserDoc = async (user: UserType) => {
// get user doc
const userDoc = await getDoc(
doc(db, 'users', user.uid)
);
// create one if DNE
if (!userDoc.exists()) {
await setDoc(doc(db, 'users', user.uid), {
displayName: user.displayName,
photoURL: user.photoURL,
email: user.email,
createdAt: serverTimestamp()
});
}
// return user
return user;
};
We can then call this function inside onIdStateChanged
.
const user = (
defaultUser: UserType | null = null
) => readable<UserType | null>(
defaultUser,
(set: Subscriber<UserType | null>) => {
return onIdTokenChanged(auth, (user: User | null) => {
if (!user) {
set(null);
return;
}
const { displayName, photoURL, uid, email } = user;
// create user doc if DNE
createUserDoc({
displayName,
photoURL,
uid,
email
}).then((_user) => {
set(_user);
});
});
}
);
š Do not use await
inside observables, effects, or stores. Instead, call the results inside the then
function. Some implementations can have undesired effects even with async
.
Other Frameworks#
You can follow the same pattern in any Framework by putting the createUserDoc
function inside onIdStateChanged
.
Server Trigger Method#
The Firebase Trigger Function is extremely simple. Add the user on user creation.
import { auth } from 'firebase-functions';
import { FieldValue, getFirestore } from 'firebase-admin/firestore';
const db = getFirestore();
export const createUser = auth.user().onCreate((user) => {
const { displayName, phoneNumber, email } = user;
return db.collection('users').doc(user.uid).create({
displayName,
phoneNumber,
email,
createdAt: FieldValue.serverTimestamp()
});
});
Feel free to add the createdAt
in any method you choose, but you can also use user.metadata.creationTime
to get the creation time anywhere in your app.
ā Firestore Functions Generation 2 do NOT provide support for Authentication Event Triggers.
Delete Trigger#
You could also delete a user from your collection with onDelete
, but I wouldnāt recommend using this. You would need to make sure you have cascade
delete that removes all the userās references in other collections (posts, tweets, comments, reviews, etc). Generally, to delete a user, you would want to soft delete or disable the user on the client side.
Server Only Method#
You could also manually create a user on the server using firebase-admin
. However, you will not have access to user methods like signInWithPopup
, as this would need to be done on the client. You would pass in the user information and create the user on the server. This could be a callable Firebase function or directly in your SSR Framework.
import { getAuth } from 'firebase-admin/auth';
...
const adminAuth = getAuth();
const userData = {
email: '[email protected]',
emailVerified: true,
displayName: 'Jonathan Gamble',
photoURL: 'https://code.build/images/code.build-1x1.webp'
};
const user = await adminAuth.createUser(userData);
createUserDoc({ ...user, uid: user.uid });
Considerations#
- You could also consider protecting your routes by checking for a user collection on the server, but you would have to be cognizant of document reads.
- If you have the
Firebase
SDK present on the client, a beginner hacker could create a newuser
without access to your server. This could potentially create a problem with your app, as theuser
would not be guaranteed to have auser
document unless you take precautions. - While the
onCreate
trigger method is the safest method, the user document will not be created immediately, so your app needs to account for the delay. - You cannot secure fields in a document, only documents themselves. A user document is great for
profile
information, but you should create a separate collection or subcollection for private user data.
J