<?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>I Deliberately Broke a Git Merge to See What Happens Under the Hood</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Fri, 30 Oct 2026 14:34:42 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/i-deliberately-broke-a-git-merge-to-see-what-happens-under-the-hood-4li</link>
      <guid>https://dev.to/anderson-devvs/i-deliberately-broke-a-git-merge-to-see-what-happens-under-the-hood-4li</guid>
      <description>&lt;h2&gt;
  
  
  I Wanted Git to Fail. And It Did.
&lt;/h2&gt;

&lt;p&gt;I know, that sounds a little crazy. Most developers spend their time trying to avoid errors, and here I was deliberately trying to create one.&lt;/p&gt;

&lt;p&gt;But I had a reason.&lt;/p&gt;

&lt;p&gt;While experimenting with GitFlow, I became curious about what actually happens when two branches introduce conflicting changes. I've seen merge conflicts before, but I wanted to understand what Git was doing rather than just follow instructions to get rid of the error.&lt;/p&gt;

&lt;p&gt;So I decided to put it to the test.&lt;/p&gt;

&lt;p&gt;I created conflicting changes on two branches, attempted to merge them, and watched Git respond:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Auto-merging conflict.txt
CONFLICT &lt;span class="o"&gt;(&lt;/span&gt;add/add&lt;span class="o"&gt;)&lt;/span&gt;: Merge conflict &lt;span class="k"&gt;in &lt;/span&gt;conflict.txt
Automatic merge failed&lt;span class="p"&gt;;&lt;/span&gt; fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it was. Git refused to complete the merge.&lt;/p&gt;

&lt;p&gt;Exactly what I wanted.&lt;/p&gt;

&lt;p&gt;But here's where things got interesting. Creating the conflict was relatively easy. Understanding why Git stopped, deciding how to resolve it, and making sure my repository ended up in the right state was a different story.&lt;/p&gt;

&lt;p&gt;This experiment took me through GitFlow branching, GitHub pull requests, squash merges, conflict resolution, and finally synchronizing the branches.&lt;/p&gt;

&lt;p&gt;Let me walk you through what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Experiment with GitFlow
&lt;/h2&gt;

&lt;p&gt;Before I could deliberately break a merge, I needed a proper branching workflow to experiment with.&lt;/p&gt;

&lt;p&gt;I created a repository called &lt;code&gt;gitflow-trunk-strategies&lt;/code&gt; and used a GitFlow-style approach with three branches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; — the primary branch representing release-ready work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/strong&gt; — the integration branch where feature changes come together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feature/user-login&lt;/code&gt;&lt;/strong&gt; — a temporary branch for developing an individual feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After initializing the repository and creating the first commit, I set up the development and feature branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; develop
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/user-login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing complicated yet.&lt;/p&gt;

&lt;p&gt;However, one thing worth remembering is that Git branches aren't separate copies of a project. They're references to commits.&lt;/p&gt;

&lt;p&gt;As new commits are introduced, those references move forward, and the branches can develop different histories.&lt;/p&gt;

&lt;p&gt;That's what makes branching useful, but it's also what creates interesting situations when you try to combine changes 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%2F2aqzyfqn9c0z7jdtpa5w.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%2F2aqzyfqn9c0z7jdtpa5w.png" alt="Git Bash terminal showing the creation of the feature/user-login branch alongside the main and develop branches in a GitFlow workflow." width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Creating the &lt;code&gt;feature/user-login&lt;/code&gt; branch.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Feature
&lt;/h3&gt;

&lt;p&gt;To make the experiment practical, I created a simple placeholder for a user-login feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'# Login feature'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; login.md

git add login.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: scaffold user login"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wasn't a complete authentication system. I only needed a real change that could be committed, pushed, and merged.&lt;/p&gt;

&lt;p&gt;The feature branch was now ahead of &lt;code&gt;develop&lt;/code&gt; by one commit.&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%2Fe1kkthw5pj9ehkdwnw9q.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%2Fe1kkthw5pj9ehkdwnw9q.png" alt="Git Bash terminal displaying the successful login feature commit and Git history, with the feature branch pointing to the new commit." width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Committing the login feature and inspecting the Git history.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A Quick Note on GitFlow vs. Trunk-Based Development
&lt;/h3&gt;

&lt;p&gt;GitFlow typically uses a long-lived &lt;code&gt;develop&lt;/code&gt; branch alongside &lt;code&gt;main&lt;/code&gt;, with feature branches created for individual changes.&lt;/p&gt;

&lt;p&gt;Trunk-based development, on the other hand, encourages developers to integrate small changes into the mainline frequently.&lt;/p&gt;

&lt;p&gt;Both approaches have their uses.&lt;/p&gt;

&lt;p&gt;The interesting part is that neither branching strategy magically prevents merge conflicts. Git still has to figure out how to combine changes when branches diverge.&lt;/p&gt;

&lt;p&gt;And sometimes, it simply can't make that decision for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the Feature into GitHub
&lt;/h2&gt;

&lt;p&gt;With the feature committed locally, I wanted to take it through a proper pull request workflow.&lt;/p&gt;

&lt;p&gt;I already had my GitHub repository connected to the local project, with &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt; pushed to the remote.&lt;/p&gt;

&lt;p&gt;I used GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) to manage the process from my terminal.&lt;/p&gt;

&lt;p&gt;First, I pushed the feature branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature/user-login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I opened a pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--base&lt;/span&gt; develop &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--head&lt;/span&gt; feature/user-login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"feat: user login scaffold"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Adds the login module skeleton."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub created Pull Request #1.&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%2Fpon0l1fhw9w268y7t8e0.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%2Fpon0l1fhw9w268y7t8e0.png" alt="GitHub pull request showing the proposed merge of feature/user-login into develop, including the pull request title and branch comparison." width="800" height="751"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pull request proposing changes from &lt;code&gt;feature/user-login&lt;/code&gt; into &lt;code&gt;develop&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One detail I paid attention to was the target branch.&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;--base develop&lt;/code&gt; because I wanted the feature to enter the integration branch first, rather than merge directly into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Used a Squash Merge
&lt;/h3&gt;

&lt;p&gt;After checking the pull request, I completed it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge feature/user-login &lt;span class="nt"&gt;--squash&lt;/span&gt; &lt;span class="nt"&gt;--delete-branch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A squash merge takes the changes introduced by the feature branch and records them as a new commit on the target branch.&lt;/p&gt;

&lt;p&gt;For example, if a feature branch contains several intermediate commits for fixes and adjustments, squashing can combine those changes into one integration commit.&lt;/p&gt;

&lt;p&gt;My feature only had one commit, but the exercise still demonstrated the workflow.&lt;/p&gt;

&lt;p&gt;The pull request was merged successfully, and the temporary feature branch was deleted.&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%2Fsxbeb5njobgtrzoii66f.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%2Fsxbeb5njobgtrzoii66f.png" alt="Git Bash terminal confirming a successful squash merge of the login feature, deletion of the feature branch, and synchronization of develop." width="778" height="736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The feature successfully squash-merged into &lt;code&gt;develop&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this point, the feature existed on &lt;code&gt;develop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;main&lt;/code&gt; was still behind.&lt;/p&gt;

&lt;p&gt;That wasn't an error. It was simply a consequence of the branching workflow. Merging into &lt;code&gt;develop&lt;/code&gt; doesn't automatically update every other branch.&lt;/p&gt;

&lt;p&gt;This distinction would become useful later in the experiment.&lt;/p&gt;

&lt;p&gt;For now, though, I had something else in mind.&lt;/p&gt;

&lt;p&gt;It was time to break a merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to Break Something: Creating the Merge Conflict
&lt;/h2&gt;

&lt;p&gt;Here's where I stopped following the normal feature workflow and started experimenting.&lt;/p&gt;

&lt;p&gt;I wanted Git to encounter two different versions of the same file and refuse to merge them automatically.&lt;/p&gt;

&lt;p&gt;The plan was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a file on &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a different version of that file independently on &lt;code&gt;develop&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Attempt to merge &lt;code&gt;main&lt;/code&gt; into &lt;code&gt;develop&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating the First Version on &lt;code&gt;main&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I switched 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 shell"&gt;&lt;code&gt;git checkout main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then created a file called &lt;code&gt;conflict.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'main line'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; conflict.txt

git add conflict.txt
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"main: add line"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file contained:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating a Different Version on &lt;code&gt;develop&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Next, I switched to &lt;code&gt;develop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created another file with the exact same name, but different content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'develop line'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; conflict.txt

git add conflict.txt
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"develop: add line"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This version contained:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;So now I had two branches that had independently added &lt;code&gt;conflict.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same filename. Different content.&lt;/p&gt;

&lt;p&gt;And both changes were already committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempting the Merge
&lt;/h3&gt;

&lt;p&gt;While on &lt;code&gt;develop&lt;/code&gt;, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git responded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CONFLICT &lt;span class="o"&gt;(&lt;/span&gt;add/add&lt;span class="o"&gt;)&lt;/span&gt;: Merge conflict &lt;span class="k"&gt;in &lt;/span&gt;conflict.txt
Automatic merge failed&lt;span class="p"&gt;;&lt;/span&gt; fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was again.&lt;/p&gt;

&lt;p&gt;The merge stopped because Git couldn't automatically reconcile the two different versions of the file.&lt;/p&gt;

&lt;p&gt;This particular situation is called an &lt;strong&gt;add/add conflict&lt;/strong&gt; because both branches independently added the same file path.&lt;/p&gt;

&lt;p&gt;And no, I hadn't damaged my repository.&lt;/p&gt;

&lt;p&gt;Git was actually protecting the merge from proceeding with an ambiguous result.&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%2F5d2gzai8eugd87md506j.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%2F5d2gzai8eugd87md506j.png" alt="VS Code's Git Bash terminal showing an add/add merge conflict after running git merge main on the develop branch. Git reports a conflict in conflict.txt, stops the automatic merge, and git status confirms that the repository is in a merging state with unresolved paths requiring manual resolution." width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Git detecting an add/add merge conflict in &lt;code&gt;conflict.txt&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the question became: what exactly was Git struggling to reconcile?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Happening Under the Hood?
&lt;/h2&gt;

&lt;p&gt;This was the part I was particularly interested in.&lt;/p&gt;

&lt;p&gt;Git doesn't simply compare two files and pick the one with the latest timestamp.&lt;/p&gt;

&lt;p&gt;For a typical three-way merge, Git considers three versions of the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The merge base:&lt;/strong&gt; The common ancestor shared by the two branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ours:&lt;/strong&gt; The version from the current branch, &lt;code&gt;develop&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theirs:&lt;/strong&gt; The version coming from &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common ancestor helps Git determine how each branch changed.&lt;/p&gt;

&lt;p&gt;In my case, &lt;code&gt;conflict.txt&lt;/code&gt; didn't exist in that common ancestor.&lt;/p&gt;

&lt;p&gt;Both branches independently introduced the file, and their contents were different.&lt;/p&gt;

&lt;p&gt;That's why Git reported an add/add conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Conflict Markers
&lt;/h3&gt;

&lt;p&gt;When I inspected &lt;code&gt;conflict.txt&lt;/code&gt;, Git had inserted markers similar to these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
develop line
=======
main line
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break that down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This marks the beginning of the current branch's version. Since I was on &lt;code&gt;develop&lt;/code&gt;, that was the content Git considered "ours."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;=======&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This separates the conflicting versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; main&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This marks the end of the incoming version from &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Git was essentially saying:&lt;/p&gt;

&lt;p&gt;"I found two competing versions of this file. I can show you both, but I need you to decide what the final result should be."&lt;/p&gt;

&lt;p&gt;And that's exactly what merge conflict resolution is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  There's More Going On Than Just Conflict Markers
&lt;/h3&gt;

&lt;p&gt;Git also tracks unresolved versions through its index.&lt;/p&gt;

&lt;p&gt;During an unresolved merge, the index can contain separate entries for the conflicting sides.&lt;/p&gt;

&lt;p&gt;For an add/add conflict like mine, the relevant entries are typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stage 2:&lt;/strong&gt; The version from the current branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 3:&lt;/strong&gt; The version from the incoming branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no stage-1 version for that file because it didn't exist in the merge base.&lt;/p&gt;

&lt;p&gt;One way to inspect those entries while a conflict is still unresolved is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-files &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't use this command during the original lab, but it's a useful way to explore the underlying conflict state in a future experiment.&lt;/p&gt;

&lt;p&gt;The conflict markers were simply the visible part of a more structured process.&lt;/p&gt;

&lt;p&gt;Git had the competing versions available. What it didn't have was enough information to choose the correct result automatically.&lt;/p&gt;

&lt;p&gt;That's where I had to step in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the Merge Without Losing the Changes
&lt;/h2&gt;

&lt;p&gt;At first, I thought resolving the conflict meant removing the markers and keeping both versions.&lt;/p&gt;

&lt;p&gt;But I wanted to understand whether that was actually how conflicts should be handled.&lt;/p&gt;

&lt;p&gt;Turns out, there's an important distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removing conflict markers doesn't automatically mean you've resolved the underlying problem correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine two developers changing the same configuration setting.&lt;/p&gt;

&lt;p&gt;One version might specify a database connection to one environment, while another points somewhere else.&lt;/p&gt;

&lt;p&gt;Keeping both settings could introduce invalid configuration or unexpected behavior.&lt;/p&gt;

&lt;p&gt;So the correct resolution depends on what the code is supposed to do.&lt;/p&gt;

&lt;p&gt;In my experiment, the file contained nothing more than two demonstration lines. Both could safely coexist.&lt;/p&gt;

&lt;p&gt;I edited &lt;code&gt;conflict.txt&lt;/code&gt; and kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;develop line
main line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I saved the file, then returned to Git Bash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telling Git the Conflict Was Resolved
&lt;/h3&gt;

&lt;p&gt;I staged the corrected file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add conflict.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then checked the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, Git could recognize that the conflict had been resolved, but the merge itself was still waiting to be completed.&lt;/p&gt;

&lt;p&gt;This was another useful lesson.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git add&lt;/code&gt; marks the resolved content as ready. It doesn't finish the merge commit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To complete the process, I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"merge: resolve conflict between main and develop"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, the merge was completed.&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%2F9wt52weryu79swehpocm.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%2F9wt52weryu79swehpocm.png" alt="Git Bash terminal showing the completed merge-conflict resolution commit and Git history after combining changes from main and develop." width="789" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The successful merge commit after resolving the conflicting file.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed in the Git History?
&lt;/h3&gt;

&lt;p&gt;Unlike the earlier squash merge, this operation produced a regular merge commit.&lt;/p&gt;

&lt;p&gt;A normal commit usually has one parent, while a merge commit typically has two.&lt;/p&gt;

&lt;p&gt;Those two parent references allow Git to record how the previously separate histories were connected.&lt;/p&gt;

&lt;p&gt;I could inspect that relationship using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one reason I find Git's history commands so useful.&lt;/p&gt;

&lt;p&gt;A command reporting success is good.&lt;/p&gt;

&lt;p&gt;Being able to inspect the resulting commit history is even better.&lt;/p&gt;

&lt;p&gt;The conflict was resolved. But I still had one final task before calling the experiment complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Merge Was Fixed. But Were My Branches Actually in Sync?
&lt;/h2&gt;

&lt;p&gt;After resolving the conflict, I moved on to documenting the lab and publishing the final changes.&lt;/p&gt;

&lt;p&gt;I created a &lt;code&gt;README.md&lt;/code&gt; covering the branching strategies, pull request workflow, and important commands used during the experiment.&lt;/p&gt;

&lt;p&gt;Then I committed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;

git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: add README for GitFlow vs Trunk branching lab"&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%2Fq8fk0cuodixrksc928lo.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%2Fq8fk0cuodixrksc928lo.png" alt="Git Bash terminal confirming the README documentation commit, recording the GitFlow and trunk-based development lab workflow." width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Documenting the experiment and committing the README.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, I pushed the updated &lt;code&gt;develop&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there was still something to address.&lt;/p&gt;

&lt;p&gt;Remember that the login feature had been integrated into &lt;code&gt;develop&lt;/code&gt;, while &lt;code&gt;main&lt;/code&gt; had remained behind?&lt;/p&gt;

&lt;p&gt;Even after resolving the conflict, the branches weren't automatically pointing to the same latest commit.&lt;/p&gt;

&lt;p&gt;I needed to synchronize them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using a Fast-Forward Merge
&lt;/h3&gt;

&lt;p&gt;I switched 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 shell"&gt;&lt;code&gt;git checkout main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;--ff-only&lt;/span&gt; develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--ff-only&lt;/code&gt; option tells Git to merge only if it can advance the current branch without creating a new merge commit.&lt;/p&gt;

&lt;p&gt;This works when the current branch's commit is already an ancestor of the target branch.&lt;/p&gt;

&lt;p&gt;Because I had merged &lt;code&gt;main&lt;/code&gt; into &lt;code&gt;develop&lt;/code&gt; during the conflict-resolution exercise, Git could now fast-forward &lt;code&gt;main&lt;/code&gt; to the latest commit on &lt;code&gt;develop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No additional merge commit was necessary.&lt;/p&gt;

&lt;p&gt;Finally, I pushed &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 shell"&gt;&lt;code&gt;git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verifying the Final State
&lt;/h3&gt;

&lt;p&gt;I didn't want to stop at a successful push message.&lt;/p&gt;

&lt;p&gt;I wanted to confirm that everything was actually where it should be.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git branch &lt;span class="nt"&gt;-vv&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git reported a clean working tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final history showed &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;, along with their corresponding remote-tracking references, pointing to the same latest commit.&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%2Frcm8cjqb7yma9oldh1ng.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%2Frcm8cjqb7yma9oldh1ng.png" alt="Git Bash terminal showing successful repository synchronization, a clean working tree, and main and develop pointing to the same latest commit." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The final Git history showing the synchronized branches.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That was the result I was looking for.&lt;/p&gt;

&lt;p&gt;The conflict was gone, the changes were committed, the documentation was pushed, and the repository history was in the expected state.&lt;/p&gt;

&lt;p&gt;Now I could actually say the experiment was finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Merge Strategies, Three Different Results
&lt;/h2&gt;

&lt;p&gt;One thing this experiment helped me understand is that not every merge operation produces the same Git history.&lt;/p&gt;

&lt;p&gt;I used three different integration approaches throughout the workflow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Merge Strategy&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Squash Merge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Combines the feature's changes into a new commit on the target branch&lt;/td&gt;
&lt;td&gt;Keeps the integration history compact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Regular Merge Commit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connects the histories of two branches with a merge commit&lt;/td&gt;
&lt;td&gt;Preserves the relationship between the branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast-Forward Merge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Advances a branch reference without creating a new merge commit&lt;/td&gt;
&lt;td&gt;Useful when the current branch is an ancestor of the target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What's interesting is that all three approaches can successfully integrate changes, but the resulting history isn't identical.&lt;/p&gt;

&lt;p&gt;With the feature branch, I used a squash merge.&lt;/p&gt;

&lt;p&gt;When resolving the intentional conflict, I created a regular merge commit.&lt;/p&gt;

&lt;p&gt;And when synchronizing &lt;code&gt;main&lt;/code&gt; with &lt;code&gt;develop&lt;/code&gt;, I used a fast-forward merge.&lt;/p&gt;

&lt;p&gt;Seeing these operations in the same repository made the differences much easier to understand.&lt;/p&gt;

&lt;p&gt;It also reinforced something important: &lt;strong&gt;the merge strategy you choose affects how future developers will read and understand the project's history.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That might not seem significant in a tiny lab repository.&lt;/p&gt;

&lt;p&gt;But in a larger project with multiple contributors, meaningful commit history becomes much more valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts: Breaking Things Can Teach You How They Work
&lt;/h2&gt;

&lt;p&gt;I started this experiment with one goal: make Git refuse a merge.&lt;/p&gt;

&lt;p&gt;And I succeeded.&lt;/p&gt;

&lt;p&gt;But what I found interesting wasn't the error itself. It was everything I had to understand to get from that error back to a clean, synchronized repository.&lt;/p&gt;

&lt;p&gt;A few lessons stood out to me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A merge conflict doesn't mean your repository is broken.&lt;/strong&gt; It means Git needs a decision it cannot make automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing conflict markers isn't enough.&lt;/strong&gt; You still need to make sure the resolved code behaves correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A successful pull request doesn't mean every branch is up to date.&lt;/strong&gt; You need to understand which branch received the changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git history deserves attention.&lt;/strong&gt; Commands like &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git branch -vv&lt;/code&gt;, and &lt;code&gt;git log --graph&lt;/code&gt; help verify the actual result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And perhaps the biggest takeaway?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting a merge to succeed isn't the goal. Understanding what was merged, why it was merged, and whether the resulting history is correct matters much more.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was a controlled experiment using simple files, not a complicated production application. But the underlying Git behavior is the same kind of behavior developers need to understand when collaborating on real projects.&lt;/p&gt;

&lt;p&gt;I'll be taking that lesson into my future software engineering and DevOps work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore the Repository
&lt;/h3&gt;

&lt;p&gt;If you'd like to inspect the commits and branching workflow yourself, the completed repository is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/enyichinedumanderson/gitflow-trunk-strategies.git" rel="noopener noreferrer"&gt;GitFlow vs. Trunk-Based Development Lab&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm curious: have you ever resolved a Git merge conflict, only to discover that the resulting code or branch history wasn't what you expected?&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how you handled it.&lt;/p&gt;

</description>
      <category>git</category>
      <category>devops</category>
      <category>github</category>
      <category>gitflow</category>
    </item>
    <item>
      <title>I kept putting off learning SQL because I thought it would be complicated. Turns out, I just needed to start writing queries.
Here's what changed my perspective. Give it a read!</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Thu, 29 Oct 2026 07:20:39 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/i-kept-putting-off-learning-sql-because-i-thought-it-would-be-complicated-turns-out-i-just-needed-3n0a</link>
      <guid>https://dev.to/anderson-devvs/i-kept-putting-off-learning-sql-because-i-thought-it-would-be-complicated-turns-out-i-just-needed-3n0a</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/anderson-devvs/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p" class="crayons-story__hidden-navigation-link"&gt;SQL Was Always Just Database Stuff to Me. Then I Started Writing Queries&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-4939191" 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/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 29&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/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p" id="article-link-4939191"&gt;
          SQL Was Always Just Database Stuff to Me. Then I Started Writing Queries
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sql"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sql&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/database"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;database&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&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/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p" 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;5&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/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              4&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;
            6 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>learning</category>
      <category>sql</category>
      <category>beginners</category>
      <category>database</category>
    </item>
    <item>
      <title>SQL Was Always Just Database Stuff to Me. Then I Started Writing Queries</title>
      <dc:creator>Anderson Chinedum</dc:creator>
      <pubDate>Thu, 29 Oct 2026 07:15:55 +0000</pubDate>
      <link>https://dev.to/anderson-devvs/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p</link>
      <guid>https://dev.to/anderson-devvs/sql-was-always-just-database-stuff-to-me-then-i-started-writing-queries-i9p</guid>
      <description>&lt;p&gt;I'd been meaning to learn SQL for quite some time.&lt;/p&gt;

&lt;p&gt;It wasn't that I had no interest in databases. I knew they were an important part of software development, but SQL was one of those things I kept pushing further down my learning list.&lt;/p&gt;

&lt;p&gt;Whenever I thought about it, I pictured tables, rows, columns, and something similar to working with Excel. I understood that databases stored information, but I hadn't really explored how developers interacted with that information.&lt;/p&gt;

&lt;p&gt;Eventually, I decided to stop postponing it.&lt;/p&gt;

&lt;p&gt;I opened Codecademy's &lt;strong&gt;Introduction to SQL&lt;/strong&gt; course and spent about an hour and fifteen minutes working through the Manipulation module.&lt;/p&gt;

&lt;p&gt;What surprised me wasn't that SQL suddenly became effortless. It was how quickly the basic concepts started making sense once I began writing the statements myself.&lt;/p&gt;

&lt;p&gt;The commands were descriptive, the examples were practical, and I could see how each operation affected the information stored in a table.&lt;/p&gt;

&lt;p&gt;Here's what stood out to me.&lt;/p&gt;




&lt;h2&gt;
  
  
  SQL Made More Sense When I Stopped Thinking About It Like Excel
&lt;/h2&gt;

&lt;p&gt;One of the first things I had to understand was what a relational database actually represents.&lt;/p&gt;

&lt;p&gt;A relational database organizes information into tables containing rows and columns.&lt;/p&gt;

&lt;p&gt;That probably sounds familiar if you've used spreadsheets before. And honestly, that's where my earlier impression of SQL came from.&lt;/p&gt;

&lt;p&gt;But there's more to it than arranging information in a grid.&lt;/p&gt;

&lt;p&gt;In a relational database, tables have defined structures, data types, and rules that determine how information should be stored. SQL provides a way to interact with that information through statements.&lt;/p&gt;

&lt;p&gt;Instead of manually finding and editing cells, you're describing an operation for the database to perform.&lt;/p&gt;

&lt;p&gt;The course used a table called &lt;code&gt;celebs&lt;/code&gt;, with columns such as &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;age&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each row represented a record, while each column represented a particular attribute.&lt;/p&gt;

&lt;p&gt;Once I understood that structure, the queries started feeling less abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First Query Was Surprisingly Straightforward
&lt;/h2&gt;

&lt;p&gt;The first statement I worked with was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just one line.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; tells the database what information to retrieve, &lt;code&gt;*&lt;/code&gt; represents all columns, and &lt;code&gt;FROM celebs&lt;/code&gt; identifies the table.&lt;/p&gt;

&lt;p&gt;In other words, I was asking the database to return all the information from that table.&lt;/p&gt;

&lt;p&gt;If I only wanted the names, I could be more specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was where I began appreciating SQL's syntax.&lt;/p&gt;

&lt;p&gt;Rather than memorizing a complicated sequence of operations, I could express what I wanted the database to return.&lt;/p&gt;

&lt;p&gt;Of course, SQL is much more than a few readable keywords. But seeing a statement produce the expected result made the language easier to approach.&lt;/p&gt;

&lt;p&gt;It also introduced an idea I found interesting: with SQL, you're often describing &lt;strong&gt;what result you want&lt;/strong&gt;, rather than spelling out every individual step the database must take to produce it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating a Table Helped Me Understand Its Structure
&lt;/h2&gt;

&lt;p&gt;Retrieving information from an existing table was one thing. Creating the table myself helped connect the pieces.&lt;/p&gt;

&lt;p&gt;One of the exercises involved writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines a new table with three columns.&lt;/p&gt;

&lt;p&gt;The column names describe the information being stored, while the data types specify what kind of values the columns should contain.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;INTEGER&lt;/code&gt; represents whole numbers and &lt;code&gt;TEXT&lt;/code&gt; represents text values.&lt;/p&gt;

&lt;p&gt;I also learned that SQL keywords are commonly written in uppercase for readability, while a semicolon marks the end of a statement.&lt;/p&gt;

&lt;p&gt;The syntax itself wasn't difficult to follow. What mattered was understanding why each part existed.&lt;/p&gt;

&lt;p&gt;Once the table had been created, it was time to add records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Information with INSERT INTO
&lt;/h3&gt;

&lt;p&gt;The next statement was &lt;code&gt;INSERT INTO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of the kind of example I practiced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sample Artist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The statement identifies the table, specifies the columns receiving values, and provides the values to insert.&lt;/p&gt;

&lt;p&gt;At this point, I could see how the operations connected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE TABLE&lt;/code&gt; establishes the structure, &lt;code&gt;INSERT INTO&lt;/code&gt; adds records, and &lt;code&gt;SELECT&lt;/code&gt; retrieves information from those records.&lt;/p&gt;

&lt;p&gt;It sounds simple when explained that way, but actually writing and running the commands helped me understand the relationship between them.&lt;/p&gt;




&lt;h2&gt;
  
  
  ALTER TABLE and UPDATE Taught Me an Important Difference
&lt;/h2&gt;

&lt;p&gt;Two commands I found particularly useful were &lt;code&gt;ALTER TABLE&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They both change something, but they operate at different levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt; changes the structure of a table.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, the course introduced a new column called &lt;code&gt;twitter_handle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;twitter_handle&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table now had an additional column, but the existing records didn't automatically have values for it.&lt;/p&gt;

&lt;p&gt;Those missing values were represented by &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UPDATE&lt;/code&gt;, on the other hand, changes information already stored in the table.&lt;/strong&gt;&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 sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;twitter_handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'@example'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes the &lt;code&gt;twitter_handle&lt;/code&gt; value for the record whose ID is &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distinction became clear to me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE&lt;/code&gt; modifies the table's definition.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE&lt;/code&gt; modifies data within the table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then there's the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;It's easy to overlook, but it determines which records an operation affects.&lt;/p&gt;

&lt;p&gt;Without the appropriate &lt;code&gt;WHERE&lt;/code&gt; condition, an &lt;code&gt;UPDATE&lt;/code&gt; statement could modify every row.&lt;/p&gt;

&lt;p&gt;That was a useful reminder that readable syntax doesn't automatically make a command safe to execute.&lt;/p&gt;

&lt;p&gt;You still need to understand exactly what you're asking the database to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  DELETE FROM: A Simple Command with Serious Consequences
&lt;/h2&gt;

&lt;p&gt;Another operation I practiced was deleting records.&lt;/p&gt;

&lt;p&gt;The course used this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;twitter_handle&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The statement removes records where &lt;code&gt;twitter_handle&lt;/code&gt; has a &lt;code&gt;NULL&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;I learned that &lt;code&gt;NULL&lt;/code&gt; represents missing or unknown data. It isn't the same thing as zero or an empty string.&lt;/p&gt;

&lt;p&gt;That's why SQL uses &lt;code&gt;IS NULL&lt;/code&gt; to test for it.&lt;/p&gt;

&lt;p&gt;Something else stood out here.&lt;/p&gt;

&lt;p&gt;Compare the query above with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;celebs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing the condition changes the effect completely. Instead of deleting selected records, the statement would delete every row in the table.&lt;/p&gt;

&lt;p&gt;That made me think more carefully about database operations.&lt;/p&gt;

&lt;p&gt;The commands themselves might be straightforward, but understanding their consequences is just as important as knowing their syntax.&lt;/p&gt;

&lt;p&gt;For more substantial databases, I'd want to inspect which records match a condition before running a destructive operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints Showed Me How Databases Protect Their Data
&lt;/h2&gt;

&lt;p&gt;Toward the end of the module, I learned about constraints.&lt;/p&gt;

&lt;p&gt;Until then, I'd mostly been working with commands for creating, retrieving, and changing information.&lt;/p&gt;

&lt;p&gt;Constraints introduced another responsibility: ensuring that the information stored in a table follows certain rules.&lt;/p&gt;

&lt;p&gt;The course covered &lt;code&gt;PRIMARY KEY&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt;, &lt;code&gt;NOT NULL&lt;/code&gt;, and &lt;code&gt;DEFAULT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One of the exercises involved creating an &lt;code&gt;awards&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;awards&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;recipient&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;award_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'Grammy'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few important details here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PRIMARY KEY&lt;/code&gt; identifies a record uniquely.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NOT NULL&lt;/code&gt; requires the &lt;code&gt;recipient&lt;/code&gt; column to contain a value.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEFAULT 'Grammy'&lt;/code&gt; supplies a predefined award name when a new record doesn't specify one.&lt;/p&gt;

&lt;p&gt;I found this particularly interesting because it showed that managing a database isn't only about inserting and retrieving information.&lt;/p&gt;

&lt;p&gt;It's also about controlling what information should be accepted in the first place.&lt;/p&gt;

&lt;p&gt;As applications grow and more records are created, those rules become increasingly important.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed My Perspective
&lt;/h2&gt;

&lt;p&gt;Looking back at the module, I think my biggest discovery was how much easier SQL became once I approached it as a language for communicating with a database.&lt;/p&gt;

&lt;p&gt;Before starting, I mostly associated it with database tables and spreadsheet-style work.&lt;/p&gt;

&lt;p&gt;After completing the introductory exercises, I could see a clearer process:&lt;/p&gt;

&lt;p&gt;You define the structure of your data, insert records, retrieve what you need, make changes when necessary, and establish rules to maintain consistency.&lt;/p&gt;

&lt;p&gt;Each command serves a purpose.&lt;/p&gt;

&lt;p&gt;And because the statements are relatively descriptive, it didn't take long for the basic syntax to feel familiar.&lt;/p&gt;

&lt;p&gt;That doesn't mean I've become proficient in SQL after one session.&lt;/p&gt;

&lt;p&gt;I've only worked through the introductory Manipulation module. I still have plenty to learn about querying multiple tables, joins, aggregations, database relationships, and writing more advanced queries.&lt;/p&gt;

&lt;p&gt;But I now understand the foundation well enough to see where those concepts fit.&lt;/p&gt;

&lt;p&gt;I'm also beginning to appreciate why SQL remains relevant beyond people who work exclusively with databases. Applications depend on stored information, and understanding how to inspect and manage that information is valuable in software development, cloud engineering, and troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I spent roughly 75 minutes working through the eleven introductory exercises, and it changed how I viewed a subject I'd been putting off for a while.&lt;/p&gt;

&lt;p&gt;SQL wasn't quite what I'd imagined.&lt;/p&gt;

&lt;p&gt;The fundamentals were more approachable, the statements were readable, and experimenting with them helped me understand how information is managed inside a relational database.&lt;/p&gt;

&lt;p&gt;The more interesting challenge now is taking those concepts beyond guided exercises.&lt;/p&gt;

&lt;p&gt;I want to create a database locally, design my own tables, work with a more realistic dataset, and see how much of what I've learned I can apply independently.&lt;/p&gt;

&lt;p&gt;That's when I'll have a better idea of how comfortable I really am with SQL.&lt;/p&gt;

&lt;p&gt;For now, I'm glad I finally started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning resource:&lt;/strong&gt; &lt;a href="https://www.codecademy.com/courses/intro-to-sql/lessons/manipulation/exercises/sql" rel="noopener noreferrer"&gt;Codecademy — Introduction to SQL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes a subject feels more complicated from a distance than it does when you actually sit down and work through the fundamentals.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <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>
