Authentication looked straightforward when all I needed was a way for users to sign in.
It became more interesting while I was building Loomwing, a custom-fashion marketplace application with two distinct user types: customers and tailors.
A customer needs a customer experience. A tailor needs a tailor experience. But I did not want returning users to choose their role every time they signed in.
Once Firebase knows who a user is, how should the application determine what kind of user they are?
The solution I settled on was to separate identity from application-specific profile data.
The application then uses that persisted role to decide which experience to load.
At a high level, I separated authentication from application-specific user data.
Firebase Authentication is responsible for identifying the user. Once a user signs in, Firebase gives the application a unique user ID — the UID.
Loomwing then uses that UID to look for a corresponding profile in Cloud Firestore:
That Firestore document contains the information Loomwing needs beyond basic authentication, including the user's display name, authentication provider, and most importantly, their application role.
This separation means the login screen does not have to decide what kind of user someone is every time they return. Once a role has been selected during account creation, the persisted Firestore profile becomes the source of truth for the application's role-based experience.
Firebase Authentication tells Loomwing who the user is. Firestore tells Loomwing what kind of user they are.
The first thing I wanted was one place where Firebase would be initialized and reused across the app.
I didn't want authentication logic scattered across different React components, so I created a small Firebase module that exports both Authentication and Firestore.
The getApps() check is there to make sure Firebase doesn't initialize the default app more than once.
My configuration values come from Vite environment variables instead of being written directly into the module.
One thing I had to understand here is that VITE_ variables are still exposed to the client bundle. They're useful for keeping configuration organized between environments, but they aren't a place for server-side secrets.
I also keep my actual .env.local file out of Git and use an .env.example file to document the variables the project expects.
