I was once an avid user of Firebase. But over time, I gradually became fond of PostgreSQL-backed open-source alternatives like Hasura or Supabase and moved on. The two things that I don’t miss in Firebase are:
- No way to change custom user claims in the dashboard
- No easy one-click solution to auto-timestamp document updates in Firestore 😤
Here are two Firebase (serverless) functions to make your life easier. Both are slight modifications of community knowledge from StackOverflow.
Mirror custom claims from a collection
You can implement granular access control in Firebase using custom claims. But guess what - there’s no way to do that on the dashboard UI. 😡 You’re forced to build your own backend with some code that looks like below (code from Firebase Admin docs):
// Set admin privilege on the user corresponding to uid.
getAuth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
A smart workaround is to create a Firestore collection and mirror any custom claims on that table using a Firebase function.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
const auth = admin.auth();
interface ClaimsDocumentData extends admin.firestore.DocumentData {
_lastCommitted?: admin.firestore.Timestamp;
}
// since Firebase auth doesn't provide a UI to set custom claims,
// use userClaims collection as a mirror
export const mirrorCustomClaims = functions.firestore
.document("userClaims/{uid}")
.onWrite(async (change, context) => {
const beforeData: ClaimsDocumentData = change.before.data() || {};
const afterData: ClaimsDocumentData = change.after.data() || {};
// skip updates where _lastCommitted field changed
// to avoid infinite loops
const skipUpdate =
beforeData._lastCommitted &&
afterData._lastCommitted &&
!beforeData._lastCommitted.isEqual(afterData._lastCommitted);
if (skipUpdate) {
return;
}
// create a new JSON payload and check that it's under
// the 1000 character limit
const { _lastCommitted, ...newClaims } = afterData;
const stringifiedClaims = JSON.stringify(newClaims);
if (stringifiedClaims.length > 1000) {
console.error(
"New custom claims object string exceeds 1000 characters",
stringifiedClaims
);
return;
}
const uid = context.params.uid;
await auth.setCustomUserClaims(uid, newClaims);
await change.after.ref.update({
_lastCommitted: admin.firestore.FieldValue.serverTimestamp(),
...newClaims,
});
});
Once you deploy the mirrorCustomClaims()
function, you can edit user custom claims in the Firestore dashboard. Each document ID in userClaims
collection should match a user’s uid. The example below sets an admin
custom claim to a user.
Auto-timestamp document updates
This is another common use-case. Add any tables you’d like to auto-timestamp to the collectionsToTimestamp
array.
export const autoTimestamp = functions.firestore
.document("{colId}/{docId}")
.onWrite(async (change, context) => {
// the collections to trigger automatic insertion of
// createdAt/modifedAt timestamps
const collectionsToTimestamp = ["myCollection1", "myCollection2"];
// do nothing if the changed collection is
// not one of the target collections
if (collectionsToTimestamp.indexOf(context.params.colId) === -1) {
return null;
}
// identify event type (create, update, or delete)
const isDocCreated = !change.before.exists && change.after.exists;
const isDocUpdated = change.before.exists && change.after.exists;
const isDocDeleted = change.before.exists && !change.after.exists;
// do nothing if the doc is deleted
if (isDocDeleted) {
return null;
}
// simplify input data
const after: any = change.after.exists ? change.after.data() : null;
const before: any = change.before.exists ? change.before.data() : null;
// prevent update loops from triggers
const canUpdate = () => {
// If update trigger
if (before.updatedAt && after.updatedAt) {
if (after.updatedAt._seconds !== before.updatedAt._seconds) {
return false;
}
}
// if create trigger
if (!before.createdAt && after.createdAt) {
return false;
}
return true;
};
// if newly created doc, add createdAt
if (isDocCreated) {
return change.after.ref
.set(
{
createdAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
)
.catch((err: any) => {
console.error(err);
return false;
});
}
// if updated doc, add updatedAt
if (isDocUpdated && canUpdate()) {
return change.after.ref
.set(
{
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
)
.catch((err: any) => {
console.error(err);
return false;
});
}
return null;
});