Custom Usernames in Firebase
Firebase assigns each user a unique ID upon signing up, but many apps require users to choose their own custom unique username, which is not an out-of-box feature in Firebase. Think of apps like Twitter or Medium, where each user’s profile can be visited on a user like example.com/{username}
. The following lesson demonstrates how to securely create and validate custom usernames for Firebase users by combining Cloud Firestore.
Assumptions:
- Each user must have a unique username.
- Username will not change after initial selection.
Data Model
The data model requires two root collections users
and usernames
. They use a simple reverse mapping (point to each other) that enables uniqueness validation.
User Profile Document
The user profile document contains public information about the user, including the username value.
Username Document
The username document has an ID that matches the username, thus guaranteeing uniqueness. This document will be fetched when the user chooses a username to inform them whether or not their choice is available.
Frontend Implementation
SignIn
First, sign in the user with any authentication method.
const auth = firebase.auth();
const firestore = firebase.firestore();
auth.signInWithPopup(someProvider)
Validate Username Selection
Create a form input where the user can type their username. After each change to the form input, make a request to Firestore to check if the username exists. If it does NOT exist, show a green check mark saying username is available. If it does exists, show an error saying username taken.
Optimization. Reduce the number of reads sent to the database by wrapping this function in a debounce to wait for the user to stop typing before sending the request.
<form>
<input id="username" />
</form>
// Your username form input
const formInput = document.getElementById('username');
// State
let usernameAvailable = false;
// Wrap in debounce to prevent unnecessary reads
formInput.onchange = debounce ( async(event) => {
// When form input changes, check existence of the matching username doc in db
const username = event.target.value;
if (username.length >= 3 && username.length <= 15) {
const ref = firestore.doc(`usernames/${username}`);
const { exists } = await ref.get();
usernameAvailable = !exists;
}
}, 500)
Commit Username as Batch Write
Both documents are created in a batch, ensuring that they will be created successfully together (or fail together).
const form = document.getElementByTagName('form');
form.onsubmit = async(username) => {
// Get the current user
const user = auth.currentUser;
// Create refs for both documents
const userDoc = firestore.doc(`users/${user.uid}`);
const usernameDoc = firestore.doc(`usernames/${username}`);
// Commit both docs together as a batch write.
const batch = firestore.batch();
batch.set(userDoc, { username });
batch.set(usernameDoc, { uid: user.uid });
await batch.commit()
}
Backend Security
The rules below use batch write tools like getAfter to fetch a document as-if the batch write has been completed. The rules solve a variety of potential security issues:
Rules for Batch Write
- Username must be unique.
- Username must be formatted properly, and be between 3 & 15 characters.
- Username cannot exist without user profile, and vice versa.
- Users cannot modify their username after creation.
- Users cannot spam the database with username docs.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
match /users/{userId} {
allow read;
allow create: if isValidUser(userId);
}
function isValidUser(userId) {
let isOwner = request.auth.uid == userId;
let username = request.resource.data.username;
let createdValidUsername = existsAfter(/databases/$(database)/documents/usernames/$(username));
return isOwner && createdValidUsername;
}
match /usernames/{username} {
allow read;
allow create: if isValidUsername(username);
}
function isValidUsername(username) {
let isOwner = request.auth.uid == request.resource.data.uid;
let isValidLength = username.size() >= 3 && username.size() <= 15;
let isValidUserDoc = getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data.username == username;
return isOwner && isValidLength && isValidUserDoc;
}
}
}
}