<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anderson Chinedum</title>
    <description>The latest articles on DEV Community by Anderson Chinedum (@anderson-devvs).</description>
    <link>https://dev.to/anderson-devvs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4045832%2Fbef6367b-d5e1-415c-b9cb-a3c6ad2d98fa.jpg</url>
      <title>DEV Community: Anderson Chinedum</title>
      <link>https://dev.to/anderson-devvs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anderson-devvs"/>
    <language>en</language>
    <item>
      <title>The DevOps Toolchain Every Beginner Needs</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Mon, 05 Oct 2026 18:16:35 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/the-devops-toolchain-every-beginner-needs-3gp3</link>
      <guid>https://dev.to/anderson-devvs/the-devops-toolchain-every-beginner-needs-3gp3</guid>
      <description>&lt;p&gt;When I started getting deeper into DevOps, I kept coming across the same tools again and again: Git, Docker, Terraform, Azure CLI, GitHub CLI, and others.&lt;/p&gt;

&lt;p&gt;Knowing their names was one thing. Understanding &lt;strong&gt;why I needed each one and how they fit into an actual DevOps workflow&lt;/strong&gt; was different.&lt;/p&gt;

&lt;p&gt;Recently, I decided to properly set up the toolchain I will be using for my DevOps work on Windows. I expected it to be mostly downloading applications and checking their versions.&lt;/p&gt;

&lt;p&gt;It wasn't quite that simple.&lt;/p&gt;

&lt;p&gt;Along the way, I ran into issues with Docker not connecting to its engine, Terraform not being recognized outside its folder, Azure CLI not appearing in an already-open terminal, and GitHub CLI working in PowerShell but not immediately inside VS Code.&lt;/p&gt;

&lt;p&gt;Working through those problems actually helped me understand the tools better than simply installing them without any issues.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through the DevOps toolchain I set up, what each tool does, how I verified that everything was working, and some of the problems I encountered along the way.&lt;/p&gt;

&lt;p&gt;One thing I should make clear from the beginning: there isn't one universal DevOps toolchain that every engineer uses.&lt;/p&gt;

&lt;p&gt;Different teams use different cloud providers, CI/CD platforms, container tools, and infrastructure technologies.&lt;/p&gt;

&lt;p&gt;For the Azure-focused environment I'm currently working with, these are the tools I started with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Azure CLI&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;li&gt;Visual Studio Code&lt;/li&gt;
&lt;li&gt;GitHub CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of the setup, I wanted more than six installed applications. I wanted a workstation where I could actually use all of them from the terminal and start connecting them together in real DevOps workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly is a DevOps toolchain?
&lt;/h2&gt;

&lt;p&gt;DevOps is not one application.&lt;/p&gt;

&lt;p&gt;It is a workflow made up of different tools that handle different parts of building, testing, deploying, and managing software.&lt;/p&gt;

&lt;p&gt;The easiest way I found to understand the tools was to stop looking at them individually and think about how they connect.&lt;/p&gt;

&lt;p&gt;For the toolchain I set up, the flow looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VS Code
   ↓
Git
   ↓
GitHub
   ↓
Docker
   ↓
Terraform
   ↓
Azure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code gives me the environment where I work.&lt;/p&gt;

&lt;p&gt;Git tracks the changes I make.&lt;/p&gt;

&lt;p&gt;GitHub gives me somewhere to store and share those changes remotely.&lt;/p&gt;

&lt;p&gt;Docker packages applications into containers.&lt;/p&gt;

&lt;p&gt;Terraform lets me describe infrastructure using code.&lt;/p&gt;

&lt;p&gt;Azure CLI lets me interact with Azure directly from the terminal.&lt;/p&gt;

&lt;p&gt;GitHub CLI also gives me another way to work with GitHub without constantly going back to the browser.&lt;/p&gt;

&lt;p&gt;Each tool solves a different problem, but together they form the beginning of a practical DevOps environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpeqz8adyz5ubttgnk26f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpeqz8adyz5ubttgnk26f.png" alt="VS Code PowerShell terminal showing successful version checks for Git, Azure CLI, Docker, Terraform, Visual Studio Code, and GitHub CLI on Windows." width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Git: tracking changes from the beginning
&lt;/h2&gt;

&lt;p&gt;Git was already installed on my system, so the first thing I needed to do was verify it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My system returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git version 2.56.0.windows.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git is a version-control system.&lt;/p&gt;

&lt;p&gt;Its job is to track changes made to files over time.&lt;/p&gt;

&lt;p&gt;That becomes important very quickly in DevOps because almost everything eventually becomes a file that needs to be tracked: application code, Dockerfiles, Terraform configuration, CI/CD pipelines, documentation, and more.&lt;/p&gt;

&lt;p&gt;If something changes, Git gives me a way to know &lt;strong&gt;what changed, when it changed, and which version I am currently working with&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is also why Git usually sits near the beginning of a DevOps workflow.&lt;/p&gt;

&lt;p&gt;Before automating or deploying something, it makes sense to first have proper control over the files being changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure CLI: working with Azure from the terminal
&lt;/h2&gt;

&lt;p&gt;The next tool was Azure CLI.&lt;/p&gt;

&lt;p&gt;Azure has a graphical web portal where resources can be created and managed, but DevOps workflows should not depend entirely on manually clicking through a dashboard.&lt;/p&gt;

&lt;p&gt;Azure CLI gives me a way to interact with Azure directly from the terminal.&lt;/p&gt;

&lt;p&gt;On Windows, I installed it using WinGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--exact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.AzureCLI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installation completed successfully.&lt;/p&gt;

&lt;p&gt;Then I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az : The term 'az' is not recognized as the name of a cmdlet,
function, script file, or operable program.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I thought something had gone wrong with the installation.&lt;/p&gt;

&lt;p&gt;But Azure CLI had actually installed correctly.&lt;/p&gt;

&lt;p&gt;The problem was that I was still using a PowerShell session that had already been open before the installation.&lt;/p&gt;

&lt;p&gt;I closed PowerShell, opened a fresh session, and tried again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time it worked and returned the installed Azure CLI version.&lt;/p&gt;

&lt;p&gt;That taught me something useful very early in the setup:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Installing a command-line tool does not always mean an already-open terminal will immediately know where to find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the terminal simply needs to be restarted so it can pick up the updated environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker: having the CLI is not enough
&lt;/h2&gt;

&lt;p&gt;Docker was one of the most interesting parts of the setup.&lt;/p&gt;

&lt;p&gt;Docker is used to package applications into containers so that they can run more consistently across different environments.&lt;/p&gt;

&lt;p&gt;I installed Docker Desktop and checked whether the CLI was available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker version 29.8.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, I assumed Docker was ready.&lt;/p&gt;

&lt;p&gt;Then I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello-world&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and got an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to connect to the docker API at
npipe:////./pipe/dockerDesktopLinuxEngine

The system cannot find the file specified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was where I learned the difference between &lt;strong&gt;having the Docker command installed&lt;/strong&gt; and &lt;strong&gt;having the Docker engine running&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Docker CLI was available, but Docker Desktop had not fully started the engine the CLI needed to communicate with.&lt;/p&gt;

&lt;p&gt;After starting Docker Desktop properly, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time I could see both client and server information.&lt;/p&gt;

&lt;p&gt;Then I tried again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello-world&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker pulled the image and successfully created the container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from Docker!

This message shows that your installation appears to be working correctly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5woiciqti6jzzmk1l81f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5woiciqti6jzzmk1l81f.png" alt="PowerShell terminal showing Docker successfully pulling and running the hello-world container, ending with the “Hello from Docker!” confirmation message." width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That test was more meaningful than simply checking the version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker --version&lt;/code&gt; proved that the command existed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker run hello-world&lt;/code&gt; proved that Docker could actually contact the daemon, pull an image, create a container, and run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform: when the application works but Windows cannot find it
&lt;/h2&gt;

&lt;p&gt;Terraform gave me a completely different problem.&lt;/p&gt;

&lt;p&gt;Terraform is an Infrastructure as Code tool.&lt;/p&gt;

&lt;p&gt;Instead of manually creating every infrastructure resource through a cloud dashboard, Terraform allows infrastructure to be defined in configuration files.&lt;/p&gt;

&lt;p&gt;For Windows, I downloaded the Terraform ZIP file and extracted the application into a folder.&lt;/p&gt;

&lt;p&gt;Then I tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PowerShell returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform : The term 'terraform' is not recognized as the name
of a cmdlet, function, script file, or operable program.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, I needed to know whether Terraform itself was broken or whether Windows simply did not know where the application was located.&lt;/p&gt;

&lt;p&gt;I changed into the folder where I had placed Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Users\&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;USER&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;\Documents\Terraform&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\terraform.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform v1.16.5
on windows_amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That told me immediately that Terraform itself was fine.&lt;/p&gt;

&lt;p&gt;The problem was &lt;code&gt;PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Windows uses the PATH environment variable to know which directories it should search when I enter commands such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git
docker
terraform
az
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added the Terraform folder to my user PATH.&lt;/p&gt;

&lt;p&gt;After closing PowerShell and opening a new session, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;again.&lt;/p&gt;

&lt;p&gt;This time it worked from anywhere.&lt;/p&gt;

&lt;p&gt;That was probably the clearest lesson I got from the entire setup about how command-line tools actually become available system-wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  VS Code: where everything starts coming together
&lt;/h2&gt;

&lt;p&gt;VS Code was already my main development environment, so there wasn't much installation work to do here.&lt;/p&gt;

&lt;p&gt;I verified that its CLI command was working with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part for me was seeing how all the other tools could come together inside the VS Code integrated terminal.&lt;/p&gt;

&lt;p&gt;Instead of constantly switching between different applications, I could use Git, Azure CLI, Docker, Terraform, and GitHub CLI from the same workspace where I was editing my files.&lt;/p&gt;

&lt;p&gt;That made the setup start feeling less like a collection of individual programs and more like an actual working environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub CLI: GitHub from the terminal
&lt;/h2&gt;

&lt;p&gt;The last major tool I added was GitHub CLI.&lt;/p&gt;

&lt;p&gt;Git and GitHub are related, but they solve different problems.&lt;/p&gt;

&lt;p&gt;Git handles version control locally.&lt;/p&gt;

&lt;p&gt;GitHub hosts repositories remotely and provides tools for collaboration.&lt;/p&gt;

&lt;p&gt;GitHub CLI allows many GitHub operations to happen directly from the terminal.&lt;/p&gt;

&lt;p&gt;After installing it, I verified the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I authenticated using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub.com
HTTPS
Login with a web browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI generated a temporary device code and completed the authentication through my browser.&lt;/p&gt;

&lt;p&gt;But I ran into one more environment problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gh&lt;/code&gt; worked correctly inside normal PowerShell, but when I tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside VS Code, the terminal still returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gh : The term 'gh' is not recognized...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closing and reopening the terminal did not solve it.&lt;/p&gt;

&lt;p&gt;What eventually worked was closing VS Code completely, opening PowerShell where &lt;code&gt;gh&lt;/code&gt; was already recognized, changing into my project directory, and launching VS Code from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once VS Code opened from that fresh PowerShell environment, its integrated terminal could also recognize GitHub CLI.&lt;/p&gt;

&lt;p&gt;That was another reminder that applications can inherit environment information when they start.&lt;/p&gt;

&lt;p&gt;If that environment changes while an application is already running, opening another terminal inside the same application may not always be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the full toolchain
&lt;/h2&gt;

&lt;p&gt;After fixing everything, I verified the complete setup from my terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, all six tools were available and working.&lt;/p&gt;

&lt;p&gt;The versions themselves are not the important part because they will change as new releases come out.&lt;/p&gt;

&lt;p&gt;What matters is that each command successfully returns a result before I depend on that tool later in a DevOps workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the setup into a real Git project
&lt;/h2&gt;

&lt;p&gt;I didn't want to finish the exercise with nothing except installed applications.&lt;/p&gt;

&lt;p&gt;I wanted the work documented and stored on GitHub.&lt;/p&gt;

&lt;p&gt;I created a project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;devops-workstation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;devops-workstation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then initialized Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created a &lt;code&gt;README.md&lt;/code&gt; documenting the tools, their versions, the commands I used to verify them, and what I learned during the setup.&lt;/p&gt;

&lt;p&gt;After that, I staged the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;README.md&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and committed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docs: add workstation setup verification"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4wpi1p21sdbpai293gw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4wpi1p21sdbpai293gw.png" alt="Visual Studio Code showing the devops-workstation project, its README file, and a PowerShell terminal confirming that the main branch is synchronized with the remote GitHub repository and the working tree is clean." width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing the project to GitHub
&lt;/h2&gt;

&lt;p&gt;With the local repository ready, I created a public GitHub repository directly from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;devops-workstation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I connected the local repository to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;YOUR_USERNAME&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;/devops-workstation.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I renamed the branch to &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and pushed the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the push, I checked the repository status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the setup was no longer something that existed only on my laptop.&lt;/p&gt;

&lt;p&gt;It had become a small documented project that I could keep on GitHub and build on later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkjqh0wi18ef9zqk9f5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjkjqh0wi18ef9zqk9f5t.png" alt="GitHub repository page for devops-workstation showing the project description, DevOps-related topics, README documentation, verified tools, and workstation setup information." width="800" height="1428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check out the repository
&lt;/h3&gt;

&lt;p&gt;You can find the completed DevOps workstation setup and README on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/enyichinedumanderson/devops-workstation" rel="noopener noreferrer"&gt;View the devops-workstation repository on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problems that taught me the most
&lt;/h2&gt;

&lt;p&gt;Looking back, the most useful parts of this setup were actually the moments when something didn't work.&lt;/p&gt;

&lt;p&gt;I ran into four main problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker was installed but the engine wasn't running
&lt;/h3&gt;

&lt;p&gt;The Docker CLI worked, but containers couldn't run until Docker Desktop had properly started the engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure CLI installed successfully but PowerShell couldn't see it
&lt;/h3&gt;

&lt;p&gt;A new PowerShell session was needed before the &lt;code&gt;az&lt;/code&gt; command became available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform only worked inside its own folder
&lt;/h3&gt;

&lt;p&gt;The application itself was fine. The missing step was adding its directory to PATH.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub CLI worked in PowerShell but not immediately in VS Code
&lt;/h3&gt;

&lt;p&gt;VS Code was still running with an older environment, so I had to launch a fresh instance after the new CLI was available.&lt;/p&gt;

&lt;p&gt;None of these were huge errors.&lt;/p&gt;

&lt;p&gt;But fixing them forced me to understand what was actually happening underneath the commands instead of just copying installation instructions until something worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from the setup
&lt;/h2&gt;

&lt;p&gt;Before doing this, I mostly thought about installing developer tools like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download → Install → Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I think about it more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install
   ↓
Make the command available
   ↓
Verify it
   ↓
Test the actual functionality
   ↓
Document the setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Docker being installed is not the same as Docker being able to run containers.&lt;/p&gt;

&lt;p&gt;Terraform existing somewhere on my laptop is not the same as being able to call &lt;code&gt;terraform&lt;/code&gt; from my terminal.&lt;/p&gt;

&lt;p&gt;Azure CLI being installed is not useful if my working environment cannot find &lt;code&gt;az&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And a local Git repository becomes much more useful once it is documented and pushed somewhere I can actually share it.&lt;/p&gt;

&lt;p&gt;This setup gave me a much clearer picture of how the different tools in a DevOps toolchain begin to connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;The next step is to start using these tools together for more practical work: containerizing applications, managing infrastructure through code, deploying resources to Azure, and eventually working with CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Setting up the toolchain may not be the most exciting part of DevOps.&lt;/p&gt;

&lt;p&gt;But before building anything serious, it helps to know that the tools underneath your workflow actually work.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>beginners</category>
      <category>docker</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Check this Article out and drop your thoughts on it</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Sat, 03 Oct 2026 13:38:06 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/check-this-article-out-and-drop-your-thoughts-on-it-4l5</link>
      <guid>https://dev.to/anderson-devvs/check-this-article-out-and-drop-your-thoughts-on-it-4l5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog" class="crayons-story__hidden-navigation-link"&gt;Building Role-Based Authentication with Firebase in a React + Vite SaaS App&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/anderson-devvs" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4045832%2Fbef6367b-d5e1-415c-b9cb-a3c6ad2d98fa.jpg" alt="anderson-devvs profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/anderson-devvs" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Anderson Chinedum
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Anderson Chinedum
                
                
              
              &lt;div id="story-author-preview-content-4786409" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/anderson-devvs" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4045832%2Fbef6367b-d5e1-415c-b9cb-a3c6ad2d98fa.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Anderson Chinedum&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog" id="article-link-4786409"&gt;
          Building Role-Based Authentication with Firebase in a React + Vite SaaS App
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/firebase"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;firebase&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/authentication"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;authentication&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;10&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              11&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>authentication</category>
      <category>react</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Role-Based Authentication with Firebase in a React + Vite SaaS App</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:22:27 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog</link>
      <guid>https://dev.to/anderson-devvs/building-role-based-authentication-with-firebase-in-a-react-vite-saas-app-4dog</guid>
      <description>&lt;p&gt;Authentication looked straightforward when all I needed was a way for users to sign in.&lt;/p&gt;

&lt;p&gt;It became more interesting while I was building Loomwing, a custom-fashion marketplace application with two distinct user types: customers and tailors.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That raised a more useful architectural question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once Firebase knows who a user is, how should the application determine what kind of user they are?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The solution I settled on was to separate identity from application-specific profile data.&lt;/p&gt;

&lt;p&gt;Firebase Authentication handles identity.&lt;/p&gt;

&lt;p&gt;Cloud Firestore stores the Loomwing profile, including the user's role.&lt;/p&gt;

&lt;p&gt;The application then uses that persisted role to decide which experience to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodx451wwzlkvizptelu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodx451wwzlkvizptelu1.png" alt="Diagram of Loomwing's authentication flow showing email or Google sign-in, Firebase Authentication, a Firestore user profile, and routing to Customer or Tailor experiences." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a high level, I separated authentication from application-specific user data.&lt;/p&gt;

&lt;p&gt;Firebase Authentication is responsible for identifying the user. Once a user signs in, Firebase gives the application a unique user ID — the UID.&lt;/p&gt;

&lt;p&gt;Loomwing then uses that UID to look for a corresponding profile in Cloud Firestore:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;users/{uid}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For now, a Loomwing user can have one of two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tailor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In simple terms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase Authentication tells Loomwing who the user is. Firestore tells Loomwing what kind of user they are.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Initializing Firebase
&lt;/h2&gt;

&lt;p&gt;The first thing I wanted was one place where Firebase would be initialized and reused across the app.&lt;/p&gt;

&lt;p&gt;I didn't want authentication logic scattered across different React components, so I created a small Firebase module that exports both Authentication and Firestore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getApps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initializeApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getFirestore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase/firestore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_AUTH_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storageBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_STORAGE_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messagingSenderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_MESSAGING_SENDER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FIREBASE_APP_ID&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firebaseApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;getApps&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;getApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requiredConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseApp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseApp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;getApps()&lt;/code&gt; check is there to make sure Firebase doesn't initialize the default app more than once.&lt;/p&gt;

&lt;p&gt;My configuration values come from Vite environment variables instead of being written directly into the module.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_FIREBASE_API_KEY
VITE_FIREBASE_AUTH_DOMAIN
VITE_FIREBASE_PROJECT_ID
VITE_FIREBASE_STORAGE_BUCKET
VITE_FIREBASE_MESSAGING_SENDER_ID
VITE_FIREBASE_APP_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I had to understand here is that &lt;code&gt;VITE_*&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;I also keep my actual &lt;code&gt;.env.local&lt;/code&gt; file out of Git and use an &lt;code&gt;.env.example&lt;/code&gt; file to document the variables the project expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an account and saving the role
&lt;/h2&gt;

&lt;p&gt;When somebody creates a Loomwing account, I need more than their email and password.&lt;/p&gt;

&lt;p&gt;I also need to know how they intend to use the platform.&lt;/p&gt;

&lt;p&gt;Right now, Loomwing has two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tailor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So during signup, the user chooses their role once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxlg1k81yyzmhx5mp14w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxlg1k81yyzmhx5mp14w.png" alt="Loomwing account creation screen where a new user enters their details and chooses either the Customer or Tailor role." width="800" height="654"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Firebase account is created first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUserWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;firebaseUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, Loomwing creates the application profile in Firestore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;authProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;serverTimestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;serverTimestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The profile lives at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users/{uid}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the Firebase Authentication account and the Loomwing profile are connected by the same UID.&lt;/p&gt;

&lt;p&gt;I liked this approach because Firebase handles the identity side, while Firestore holds information that belongs specifically to Loomwing.&lt;/p&gt;

&lt;p&gt;The role is one of those pieces of application-specific information.&lt;/p&gt;

&lt;p&gt;It is stored in Firestore, not as a Firebase custom claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing in without asking for a role again
&lt;/h2&gt;

&lt;p&gt;This was one of the first things I wanted to clean up from the earlier version of Loomwing.&lt;/p&gt;

&lt;p&gt;During development, it can be tempting to have buttons like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sign in as Customer
Sign in as Tailor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes testing easy, but I didn't want that to become part of the real authentication system.&lt;/p&gt;

&lt;p&gt;Once an account already exists, the user shouldn't have to keep telling Loomwing what role they belong to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktnwqthr7tyju370vwhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktnwqthr7tyju370vwhb.png" alt="Loomwing sign-in screen with email and password authentication, password reset, and Continue with Google." width="799" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Email/password sign-in uses Firebase normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithEmailAndPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After Firebase authenticates the account, Loomwing reads the profile using the user's UID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email + Password
       ↓
Firebase Authentication
       ↓
Firebase UID
       ↓
users/{uid}
       ↓
Saved role
       ↓
Customer or Tailor experience
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sign-in form doesn't decide the user's role. The stored profile does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That felt much cleaner than making role selection part of every login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Sign-In was a little more complicated
&lt;/h2&gt;

&lt;p&gt;Email/password signup already gives me a chance to ask whether someone is joining as a customer or tailor.&lt;/p&gt;

&lt;p&gt;Google Sign-In doesn't.&lt;/p&gt;

&lt;p&gt;Firebase can authenticate the Google account, but it still doesn't know what that person is supposed to be inside Loomwing.&lt;/p&gt;

&lt;p&gt;The authentication itself is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signInWithPopup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleAuthProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveFirebaseUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part happens afterwards.&lt;/p&gt;

&lt;p&gt;Loomwing checks whether the authenticated Firebase user already has a profile at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users/{uid}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the profile exists, I load it and continue.&lt;/p&gt;

&lt;p&gt;If it doesn't exist, the user is asked to choose a role before the Loomwing profile is created.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Continue with Google
        ↓
Firebase authenticates user
        ↓
Does users/{uid} exist?
       /        \
     Yes         No
      ↓           ↓
Load profile   Ask for role
                  ↓
             Create profile
                  ↓
               Continue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I initially thought of this as a "new Google user" problem, but that's not quite accurate.&lt;/p&gt;

&lt;p&gt;What really matters is whether the Loomwing profile exists.&lt;/p&gt;

&lt;p&gt;The implementation doesn't depend on Google's &lt;code&gt;isNewUser&lt;/code&gt; metadata. It checks Firestore.&lt;/p&gt;

&lt;p&gt;That also covers cases where a Firebase account exists but the Loomwing profile is missing.&lt;/p&gt;

&lt;p&gt;If the user closes the onboarding flow without finishing it, Loomwing signs them back out instead of leaving the app stuck in a half-finished authentication state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firebase identity alone wasn't enough
&lt;/h2&gt;

&lt;p&gt;This is probably the part of the implementation that changed how I think about authentication the most.&lt;/p&gt;

&lt;p&gt;At first, it's easy to think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Firebase says the user is signed in, so the application session is ready.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Loomwing, that isn't always true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bpguryj18gufverdh7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bpguryj18gufverdh7c.png" alt="Firebase Authentication users dashboard showing Loomwing accounts created with Google and email-password providers while emails and user IDs are hidden." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application needs both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Firebase authenticated user
        +
valid Firestore users/{uid} profile
        =
Loomwing application user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Loomwing reads the Firestore document, it doesn't just accept whatever data happens to be there.&lt;/p&gt;

&lt;p&gt;The profile is validated.&lt;/p&gt;

&lt;p&gt;The role must be either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tailor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authentication provider must also be either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;google
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The saved profile is then converted into the user object Loomwing actually works with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toApplicationUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FirebaseUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoomwingUserProfile&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photoURL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line that matters most here is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That persisted role becomes part of the real application session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restoring the session after refresh
&lt;/h2&gt;

&lt;p&gt;The next thing I had to handle was refreshes.&lt;/p&gt;

&lt;p&gt;Signing in successfully isn't very useful if refreshing the browser suddenly makes the application behave like the user is logged out.&lt;/p&gt;

&lt;p&gt;Firebase gives us &lt;code&gt;onAuthStateChanged()&lt;/code&gt; for this.&lt;/p&gt;

&lt;p&gt;Loomwing subscribes to it when the app starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onAuthStateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRevision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;disposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signed-out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveFirebaseUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;disposed&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;currentRevision&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;revision&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when Loomwing loads, the process is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application starts
       ↓
Wait for Firebase
       ↓
Auth state resolves
       ↓
Authenticated?
    /       \
   No        Yes
   ↓          ↓
Landing    Read profile
              ↓
         Restore session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app stays in a loading state until Firebase has resolved that initial authentication state.&lt;/p&gt;

&lt;p&gt;There is also a small detail in this implementation that I think is worth mentioning.&lt;/p&gt;

&lt;p&gt;Reading the Firestore profile is asynchronous.&lt;/p&gt;

&lt;p&gt;That means an older profile request could theoretically finish after a newer authentication event.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;revision&lt;/code&gt; value prevents that old result from replacing the newer state.&lt;/p&gt;

&lt;p&gt;It's a small piece of code, but it's the kind of thing I probably wouldn't have thought much about when I was only building basic login screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firestore rules are part of the auth system too
&lt;/h2&gt;

&lt;p&gt;Getting somebody signed in is only one part of the job.&lt;/p&gt;

&lt;p&gt;I also had to think about what that authenticated user should actually be allowed to read or change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0v8o0gvj1ykghm5fmfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0v8o0gvj1ykghm5fmfu.png" alt="Firebase Authentication users dashboard showing Loomwing accounts created with Google and email-password providers while emails and user IDs are hidden." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;users&lt;/code&gt; collection, Loomwing first checks that the signed-in Firebase user owns the document being accessed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function isSignedInAs(uid) {
  return request.auth != null
    &amp;amp;&amp;amp; request.auth.uid == uid;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The profile rules then use that check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match /users/{uid} {
  allow read: if isSignedInAs(uid);

  allow create: if isSignedInAs(uid)
    &amp;amp;&amp;amp; hasValidProfileFields(uid)
    &amp;amp;&amp;amp; request.resource.data.createdAt == request.time
    &amp;amp;&amp;amp; request.resource.data.updatedAt == request.time;

  allow update: if isSignedInAs(uid)
    &amp;amp;&amp;amp; hasValidProfileFields(uid)
    &amp;amp;&amp;amp; request.resource.data.diff(resource.data).affectedKeys()
      .hasOnly(['email', 'displayName', 'updatedAt'])
    &amp;amp;&amp;amp; request.resource.data.updatedAt == request.time;

  allow delete: if false;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normal profile update can change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email
displayName
updatedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it can't change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uid
role
authProvider
createdAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means somebody can't use a normal client-side profile update to turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: "customer"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: "tailor"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't want the role to be nothing more than a frontend setting.&lt;/p&gt;

&lt;p&gt;Once the role affects what somebody can do inside the application, it also has to be treated as part of the application's security model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication doesn't mean onboarding is finished
&lt;/h2&gt;

&lt;p&gt;I ran into another interesting case when I started testing real Tailor accounts.&lt;/p&gt;

&lt;p&gt;A user could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authenticated: yes
Role: tailor
Tailor profile: missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those aren't the same thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo0nzqufw5y2jdksf36s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo0nzqufw5y2jdksf36s.png" alt="Loomwing Tailor dashboard showing the Set up your tailor profile onboarding state with opportunities, proposals, and orders locked until profile completion." width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Earlier versions of the app had mock tailor data available, but I didn't want a real authenticated Tailor to suddenly inherit some placeholder profile just because a real profile hadn't been created yet.&lt;/p&gt;

&lt;p&gt;So Loomwing looks for a TailorProfile that actually belongs to the authenticated user.&lt;/p&gt;

&lt;p&gt;If it doesn't find one, the result stays &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of showing fake information, the application shows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Set up your tailor profile&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Opportunities, proposals, and orders stay unavailable until that real profile exists.&lt;/p&gt;

&lt;p&gt;The actual profile editor is still a later product step.&lt;/p&gt;

&lt;p&gt;And I'm okay with that.&lt;/p&gt;

&lt;p&gt;I'd rather the app clearly show that something isn't finished than quietly attach placeholder information to a real account.&lt;/p&gt;

&lt;p&gt;This also helped me separate three ideas that I used to think of as one thing:&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Who is this person?&lt;/p&gt;

&lt;h3&gt;
  
  
  Application role
&lt;/h3&gt;

&lt;p&gt;Are they a customer or tailor?&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding
&lt;/h3&gt;

&lt;p&gt;Do they have all the information needed to use the features for that role?&lt;/p&gt;

&lt;p&gt;A user can successfully complete authentication and still have onboarding work left to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying it with Firebase Hosting
&lt;/h2&gt;

&lt;p&gt;Getting everything working locally was one thing.&lt;/p&gt;

&lt;p&gt;Getting it working after deployment exposed a couple of configuration mistakes.&lt;/p&gt;

&lt;p&gt;The first one was the Hosting directory.&lt;/p&gt;

&lt;p&gt;Loomwing uses Vite, so the production build goes into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Firebase Hosting therefore needs to serve that folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hosting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rewrites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/index.html"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rewrite sends application routes back through &lt;code&gt;index.html&lt;/code&gt;, which is what I need for the single-page application.&lt;/p&gt;

&lt;p&gt;I also ran into an issue when deploying my Firestore rules.&lt;/p&gt;

&lt;p&gt;At first, Hosting existed in &lt;code&gt;firebase.json&lt;/code&gt;, but Firestore wasn't configured there.&lt;/p&gt;

&lt;p&gt;So Firebase had no Firestore deployment target.&lt;/p&gt;

&lt;p&gt;The configuration needed this as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"firestore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firestore.rules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indexes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firestore.indexes.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that was in place, I could build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and deploy Hosting and Firestore rules together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase deploy &lt;span class="nt"&gt;--only&lt;/span&gt; &lt;span class="s2"&gt;"hosting,firestore:rules"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither mistake was especially complicated once I understood what was wrong.&lt;/p&gt;

&lt;p&gt;But they reminded me that getting a feature working on localhost is only part of finishing it.&lt;/p&gt;

&lt;p&gt;Deployment is part of the system too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;When I started working on this, I mostly thought of authentication as the part where somebody enters an email and password and gets access to the app.&lt;/p&gt;

&lt;p&gt;After building this version of Loomwing, I don't really see it that way anymore.&lt;/p&gt;

&lt;p&gt;The login form is probably the easiest part.&lt;/p&gt;

&lt;p&gt;The more important questions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is this person?&lt;/li&gt;
&lt;li&gt;What kind of Loomwing user are they?&lt;/li&gt;
&lt;li&gt;What information do I trust?&lt;/li&gt;
&lt;li&gt;What should they be allowed to change?&lt;/li&gt;
&lt;li&gt;What happens when they're authenticated but their profile isn't complete?&lt;/li&gt;
&lt;li&gt;What happens when they refresh the page?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model I ended up with is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Firebase Authentication
"Who are you?"

Firestore profile
"What kind of Loomwing user are you?"

Application logic
"What should you be able to do?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separating those responsibilities made the whole system easier for me to reason about.&lt;/p&gt;

&lt;p&gt;It also gives me a cleaner foundation for the next parts of Loomwing, especially proper TailorProfiles, requests, proposals, measurements, orders, and the other role-specific workflows that will sit on top of this authentication layer.&lt;/p&gt;

&lt;p&gt;There's still a lot I want to improve, but this version is a long way from the mock authentication flow I started with.&lt;/p&gt;

&lt;p&gt;And more importantly, I understand why it's structured this way now.&lt;/p&gt;

&lt;p&gt;If you're building a multi-role application with Firebase, I'd be interested to know how you've handled the separation between authentication, roles, and onboarding.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>react</category>
      <category>webdev</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
