<?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: edmondgi</title>
    <description>The latest articles on DEV Community by edmondgi (@edgi).</description>
    <link>https://dev.to/edgi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F399031%2F8cd6e54a-8ad8-443f-90ed-c574aa371044.jpeg</url>
      <title>DEV Community: edmondgi</title>
      <link>https://dev.to/edgi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edgi"/>
    <language>en</language>
    <item>
      <title>Why Did You Choose That Database For Your Application?</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Thu, 11 Dec 2025 23:51:51 +0000</pubDate>
      <link>https://dev.to/edgi/why-did-you-choose-that-database-for-your-application-2708</link>
      <guid>https://dev.to/edgi/why-did-you-choose-that-database-for-your-application-2708</guid>
      <description>&lt;h3&gt;
  
  
  A confession regarding Resume-Driven Development, the "Google Scale" fallacy, and why we always just end up using Postgres anyway.
&lt;/h3&gt;

&lt;p&gt;There is a sacred ritual that occurs at the beginning of every new software project. &lt;/p&gt;

&lt;p&gt;You pour a fresh cup of coffee, you crack your knuckles, you type &lt;code&gt;mkdir my-next-billion-dollar-idea&lt;/code&gt;, and then... you freeze.&lt;/p&gt;

&lt;p&gt;You have hit &lt;strong&gt;The Wall of Persistence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is time to choose a database. And let’s be honest, the answer you give during the System Design interview is rarely the &lt;em&gt;actual&lt;/em&gt; reason you picked the database for your side project.&lt;/p&gt;

&lt;p&gt;If we were being 100% honest, most architectural decision documents would read: &lt;em&gt;"We chose MongoDB because the lead developer is terrified of JOIN statements,"&lt;/em&gt; or &lt;em&gt;"We chose Cassandra because we wanted to feel like we work at Netflix."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But you need to make an actual choice. Let’s explore the chaotic decision tree of picking a database, the lies we tell ourselves, and how to actually pick the right one without crying.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Horsemen of Bad Database Decisions
&lt;/h2&gt;

&lt;p&gt;Before we talk about what you &lt;em&gt;should&lt;/em&gt; do, let’s look at the traps we all fall into.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Google Scale" Delusion
&lt;/h3&gt;

&lt;p&gt;You are building a To-Do list app for your cat. You expect, realistically, three users: You, your partner, and the test account you created.&lt;/p&gt;

&lt;p&gt;Yet, you find yourself thinking: &lt;em&gt;"But what about sharding? What if my cat becomes an influencer and I need write-availability across three availability zones in AWS us-east-1?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality Check:&lt;/strong&gt; You do not have Big Data. You have Small Data. You have "fits in RAM" data. You probably have "fits in a text file" data. Do not optimize for problems you hope to have in five years. Optimize for shipping next Tuesday.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Resume-Driven Development (RDD)
&lt;/h3&gt;

&lt;p&gt;You see a new vector database on Hacker News. It’s written in Rust. The logo is a cool geometric fox. You have no idea what a vector embedding is, but you know that if you put it on your CV, a recruiter might accidentally offer you $200k.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality Check:&lt;/strong&gt; Boring technology makes money. Exciting technology makes outages.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The "Schema-less" Trap
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;"I don't want to define a schema,"&lt;/em&gt; you say. &lt;em&gt;"I want to move fast and break things."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So you pick a Document store. Six months later, your database contains &lt;code&gt;user_id&lt;/code&gt; as a string in half the documents, an integer in the other half, and &lt;code&gt;null&lt;/code&gt; in the ones created on that Tuesday you were tired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality Check:&lt;/strong&gt; You always have a schema. It’s either enforced by the database (Good) or it’s enforced by a mess of &lt;code&gt;if (typeof x === 'undefined')&lt;/code&gt; statements in your application code (Bad).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Menu: A Guide for the Perplexed
&lt;/h2&gt;

&lt;p&gt;Okay, jokes aside. You actually need to store data. Here is the breakdown of when to use what, stripped of the marketing jargon.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Relational Databases (SQL)
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The contenders: PostgreSQL, MySQL, MariaDB&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vibe:&lt;/strong&gt; The responsible older sibling who does their taxes on time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; 95% of the time. Seriously. If your data has relationships (Users have Orders, Orders have Items), use SQL. It guarantees ACID compliance (your money doesn't disappear during a transaction) and it enforces structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why people avoid it:&lt;/strong&gt; You have to learn SQL.&lt;br&gt;
&lt;strong&gt;Why you should use it:&lt;/strong&gt; SQL is the lingua franca of data. It will survive the heat death of the universe.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Just use &lt;strong&gt;PostgreSQL&lt;/strong&gt;. It supports JSON blobs now. It’s basically a relational database and a NoSQL database in a trench coat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Document Stores (NoSQL)
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The contenders: MongoDB, Firestore, DynamoDB&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vibe:&lt;/strong&gt; A chaotic artist’s loft. Throw everything in a pile; we’ll sort it out later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are prototyping and the data structure changes daily.&lt;/li&gt;
&lt;li&gt;Your data is truly document-centric (e.g., storing a blog post with comments and tags as a single object).&lt;/li&gt;
&lt;li&gt;Read-heavy workloads where you just want to grab an ID and get a giant JSON blob back instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Warning:&lt;/strong&gt; Relational data in a non-relational database is a circle of hell reserved for people who enjoy writing nested loops in application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Key-Value Stores
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The contenders: Redis, Memcached&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vibe:&lt;/strong&gt; An adrenaline junkie on a caffeine drip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Caching. Session management. Leaderboards.&lt;br&gt;
&lt;strong&gt;Do not use it as:&lt;/strong&gt; Your primary source of truth. If the server restarts and you lose your Redis instance, and that’s where your user accounts were... congratulations, you no longer have users.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Graph Databases
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The contenders: Neo4j, Amazon Neptune&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vibe:&lt;/strong&gt; That conspiracy theory board with red string connecting thumbtacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; When the &lt;em&gt;relationships&lt;/em&gt; between data are more important than the data itself. Social networks (friends of friends), fraud detection rings, or recommendation engines.&lt;br&gt;
&lt;strong&gt;The Math:&lt;/strong&gt; If your SQL query has 14 &lt;code&gt;JOIN&lt;/code&gt; statements and takes 20 seconds to run, you probably need a graph DB.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Verdict
&lt;/h2&gt;

&lt;p&gt;How do you choose? Here is a simple algorithm to save you weeks of research:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Do you need to perform complex searches across vectors for an AI app?&lt;/strong&gt; Use a Vector DB (Pinecone, Weaviate, or pgvector).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Is your data literally a social graph?&lt;/strong&gt; Use a Graph DB.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Do you have 50 million writes per second from IoT sensors?&lt;/strong&gt; Use a Time-Series DB.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;For literally everything else?&lt;/strong&gt; Use a Relational Database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;"But which relational database?"&lt;/strong&gt; I hear you ask.&lt;/p&gt;

&lt;p&gt;Pick the one you know how to host. If you don't know how to host any of them, pick &lt;strong&gt;Postgres&lt;/strong&gt;, buy a managed instance for $15 a month, and go build your actual feature.&lt;/p&gt;

&lt;p&gt;Your users don't care about your database. &lt;br&gt;
They care that the login button works.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>How to Build a Professional Next.js Blog with a Secure CMS</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Thu, 11 Dec 2025 10:36:04 +0000</pubDate>
      <link>https://dev.to/edgi/how-to-build-a-professional-nextjs-blog-with-a-secure-cms-40o6</link>
      <guid>https://dev.to/edgi/how-to-build-a-professional-nextjs-blog-with-a-secure-cms-40o6</guid>
      <description>&lt;p&gt;Building a blog is often the "Hello World" of web development, but building a &lt;em&gt;production-grade&lt;/em&gt; publication platform is a different beast. You need a fast, SEO-friendly public site for readers and a secure, powerful dashboard for your editorial team.&lt;/p&gt;

&lt;p&gt;In this tutorial, we will build "TechChronicles," a dual-interface application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Public Front:&lt;/strong&gt; A high-performance Next.js app for readers.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Admin Console:&lt;/strong&gt; A restricted area for editors to draft, review, and publish content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We will focus on the architecture, the data flow, and solving the critical challenge of managing two distinct user types (Subscribers vs. Editors) without building two separate backends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: The Architecture
&lt;/h2&gt;

&lt;p&gt;We want to avoid the "Monolithic WordPress" trap. &lt;br&gt;
We also want to avoid over-engineering microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14 (App Router)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Backend:&lt;/strong&gt; Node.js + Express (API)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL + Prisma ORM&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Auth:&lt;/strong&gt; Rugi Auth (Centralized Identity)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The "Dual-Face" Strategy
&lt;/h3&gt;

&lt;p&gt;Instead of mixing logic, we treat the "Public User" and the "Admin User" as different contexts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;reader.techchronicles.com&lt;/code&gt; -&amp;gt; Optimized for reading, caching, and speed.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;admin.techchronicles.com&lt;/code&gt; -&amp;gt; Optimized for editing, data management, and security.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Phase 2: Project Setup &amp;amp; Data Modeling
&lt;/h2&gt;

&lt;p&gt;Let's start by defining our data. We need to store posts, but we also need to know &lt;em&gt;who&lt;/em&gt; wrote them.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The Schema
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;schema.prisma&lt;/code&gt;, we define a &lt;code&gt;Post&lt;/code&gt; that links to an &lt;code&gt;authorId&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// prisma/schema.prisma

model Post {
  id        String   @id @default(uuid())
  title     String
  slug      String   @unique
  content   String   // Markdown or HTML
  published Boolean  @default(false)
  authorId  String   // The Rugi Auth User ID
  createdAt DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The API Server
&lt;/h3&gt;

&lt;p&gt;Set up a simple Express server to serve these posts.&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="c1"&gt;// src/server.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&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;prisma&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;./db&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Public Endpoint: Everyone can see published posts&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/posts&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Admin Endpoint: Create a post (Wait, we need to protect this!)&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/posts&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// TODO: Verify if the user is actually an editor&lt;/span&gt;
  &lt;span class="c1"&gt;// const post = await prisma.post.create(...)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 3: The Authentication Layer
&lt;/h2&gt;

&lt;p&gt;Here is where most tutorials get bogged down. You start implementing "Sign up", "Login", "Forgot Password", "Email Verification", "JWT signing"... and suddenly your blog project is an auth project.&lt;/p&gt;

&lt;p&gt;We will skip all that boilerplate by using &lt;a href="https://www.npmjs.com/package/rugi-auth" rel="noopener noreferrer"&gt;Rugi Auth&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialize Rugi Auth
&lt;/h3&gt;

&lt;p&gt;Run the initializer in a separate folder or container alongside your API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx rugi-auth init auth-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Configure the "Dual" Apps
&lt;/h3&gt;

&lt;p&gt;This is the secret sauce. We don't just create one "App". We create two distinct logical apps in Rugi Auth that share the same user pool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;App 1: TechChronicles Public&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Goal:&lt;/strong&gt; Allow readers to subscribe/comment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Type:&lt;/strong&gt; Public&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Roles:&lt;/strong&gt; &lt;code&gt;SUBSCRIBER&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;App 2: TechChronicles Admin&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Goal:&lt;/strong&gt; Allow staff to write news.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Type:&lt;/strong&gt; Confidential&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Roles:&lt;/strong&gt; &lt;code&gt;EDITOR&lt;/code&gt;, &lt;code&gt;ADMIN&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Integrating the Protection
&lt;/h3&gt;

&lt;p&gt;Now, back to our API. We can secure that &lt;code&gt;POST&lt;/code&gt; route.&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="c1"&gt;// src/middleware/auth.ts&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;verifyToken&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;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Your standard JWT verification&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;requireEditor&lt;/span&gt; &lt;span class="o"&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if they have the 'EDITOR' role specifically for the Admin App&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminRole&lt;/span&gt; &lt;span class="o"&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;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;process&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;ADMIN_APP_ID&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 
    &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EDITOR&lt;/span&gt;&lt;span class="dl"&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;adminRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Editors only.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;h2&gt;
  
  
  Phase 4: Building the Frontends
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Public Reader Site (Next.js)
&lt;/h3&gt;

&lt;p&gt;This is standard Next.js. We fetch the published posts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&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;posts&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://api/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Latest News&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h3&gt;
  
  
  The Admin Dashboard (React/Next.js)
&lt;/h3&gt;

&lt;p&gt;This is where Rugi Auth shines. We need a login screen.&lt;/p&gt;

&lt;p&gt;Instead of coding form state, error handling, and validation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create a "Login" button.&lt;/li&gt;
&lt;li&gt; Redirect the user to your Rugi Auth hosted login page:
&lt;code&gt;http://localhost:7100/auth/login?client_id=ADMIN_APP_ID&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When they successfully log in as an Editor, they are redirected back to your dashboard with a secure token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/admin/dashboard/page.tsx&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This page is only accessible if you have a valid token&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useApi&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Editor Dashboard&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New Article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Publish Article
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h2&gt;
  
  
  Why This Approach Wins
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Separation of Concerns:&lt;/strong&gt; Your public blog code doesn't know about "admin rights". It just displays content. 
Your API handles the gatekeeping.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Unified Identity:&lt;/strong&gt; If a Subscriber is promoted to an Editor, you just add a role in Rugi Auth. 
You don't need to migrate their account.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Speed:&lt;/strong&gt; We built a secure, role-protected CMS backend in minutes by offloading the complex user management logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Add a "Comments" section to the public site, requiring the &lt;code&gt;SUBSCRIBER&lt;/code&gt; role.&lt;/li&gt;
&lt;li&gt;  Add an "Admin-Only" route to manage users (which proxies requests to Rugi Auth's API).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building a Scalable E-Commerce Platform: From Storefront to Warehouse</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Thu, 11 Dec 2025 10:17:16 +0000</pubDate>
      <link>https://dev.to/edgi/building-a-scalable-e-commerce-platform-from-storefront-to-warehouse-e7a</link>
      <guid>https://dev.to/edgi/building-a-scalable-e-commerce-platform-from-storefront-to-warehouse-e7a</guid>
      <description>&lt;p&gt;In the world of e-commerce, trust is currency. &lt;br&gt;
Your customers need to feel safe entering their details, and your operations team needs secure access to order data without risking a data breach.&lt;/p&gt;

&lt;p&gt;This guide walks you through architecting a full-stack e-commerce solution called "MarketMinds". We will build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Storefront:&lt;/strong&gt; Where customers browse and buy.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Ops Dashboard:&lt;/strong&gt; Where support staff manage orders.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Command Center:&lt;/strong&gt; Where super admins configured global settings.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Step 1: The Core Data Model
&lt;/h2&gt;

&lt;p&gt;E-commerce data is relational. We have Users, Products, and Orders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// schema.prisma

model Product {
  id          String   @id @default(uuid())
  name        String
  price       Decimal
  stock       Int
  orders      OrderItem[]
}

model Order {
  id        String    @id @default(uuid())
  userId    String    // The customer's ID
  total     Decimal
  status    String    // PENDING, SHIPPED, DELIVERED
  items     OrderItem[]
}

model OrderItem {
  orderId   String
  productId String
  quantity  Int

  order   Order   @relation(fields: [orderId], references: [id])
  product Product @relation(fields: [productId], references: [id])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;userId&lt;/code&gt; in the &lt;code&gt;Order&lt;/code&gt; model. We strictly decouple our "Business Data" (Orders) from "Identity Data" (Passwords/Emails). This is a security best practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Designing the Storefront API
&lt;/h2&gt;

&lt;p&gt;Customers need to browse products (Public) and place orders (Authenticated).&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="c1"&gt;// routes/storefront.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// PUBLIC: Anyone can view products&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// PROTECTED: Only logged-in customers can buy&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Extracted from token&lt;/span&gt;
  &lt;span class="c1"&gt;// Business logic: create order, decrement stock, charge payment...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Authentication Puzzle
&lt;/h3&gt;

&lt;p&gt;We need customers to sign up, verify email, and log in. Building this from scratch is high-risk. &lt;br&gt;
If you mess up password hashing or session storage, your store is finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter Rugi Auth.&lt;/strong&gt;&lt;br&gt;
We treat it as our dedicated Identity Provider.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create the 'MarketMinds Storefront' App&lt;/strong&gt; in Rugi Auth.&lt;/li&gt;
&lt;li&gt; Enable &lt;strong&gt;"Subscribers"&lt;/strong&gt; or &lt;strong&gt;"Customers"&lt;/strong&gt; role by default.&lt;/li&gt;
&lt;li&gt; Set up &lt;strong&gt;Google OAuth&lt;/strong&gt; in the Rugi Auth settings so customers can sign in with one click.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, &lt;code&gt;requireAuth&lt;/code&gt; just verifies the JWT signature.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: The Support Dashboard (Internal Tool)
&lt;/h2&gt;

&lt;p&gt;Now comes the complex part. You hire a Customer Support agent, Alice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Alice needs to see &lt;em&gt;all&lt;/em&gt; orders (unlike a customer).&lt;/li&gt;
&lt;li&gt;  Alice should &lt;em&gt;not&lt;/em&gt; be able to change the database schema or delete products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We create a second app in Rugi Auth: &lt;strong&gt;"MarketMinds Internal"&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Implementing Role-Based Access Control (RBAC)
&lt;/h3&gt;

&lt;p&gt;We define strictly scoped roles for this internal app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;SUPPORT_AGENT&lt;/code&gt;: Can view orders, process refunds.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;INVENTORY_MGR&lt;/code&gt;: Can update product stock and prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Backend Logic:&lt;/strong&gt;&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="c1"&gt;// routes/admin.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to check specific permissions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assertRole&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="c1"&gt;// Check if the user has this role for the INTERNAL app&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;req&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;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Route: Refund an order&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id/refund&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nf"&gt;assertRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPPORT_AGENT&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// Logic to refund order&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`User &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&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;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; refunded order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Refunded&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="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Route: Update Inventory&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products/:id/stock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;assertRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVENTORY_MGR&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// Logic to update stock&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;h2&gt;
  
  
  Step 4: The Super Admin (Founders)
&lt;/h2&gt;

&lt;p&gt;You don't want your Support Agents to have &lt;code&gt;Global Admin&lt;/code&gt; power.&lt;br&gt;
In Rugi Auth, you can create a specialized role, or even a separate &lt;code&gt;Command Center&lt;/code&gt; app, that is only accessible to you.&lt;/p&gt;

&lt;p&gt;This isolation is powerful. If a Support Agent's laptop is compromised and their token is stolen, the attacker &lt;strong&gt;cannot&lt;/strong&gt; access the "Delete Database" endpoints because that token is scoped only to the &lt;code&gt;SUPPORT_AGENT&lt;/code&gt; role, not &lt;code&gt;SUPER_ADMIN&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Frontend Integration
&lt;/h2&gt;

&lt;p&gt;For your internal dashboard, you normally want a quick, data-heavy UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Magic Link" Effect:&lt;/strong&gt;&lt;br&gt;
Because you are using a centralized auth system, your employees can have a seamless experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Alice logs into the &lt;strong&gt;Inventory App&lt;/strong&gt; to update stock.&lt;/li&gt;
&lt;li&gt; She gets a customer complaint and clicks "View Order in Support Portal".&lt;/li&gt;
&lt;li&gt; She is redirected to the &lt;strong&gt;Support App&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Rugi Auth recognizes her session&lt;/strong&gt; and automatically logs her in (SSO), provided she has permissions for the Support App.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This frictionless flow improves employee productivity while maintaining strict security boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By separating your E-Commerce platform into distinct logical "Apps" (Storefront, Internal Tools), you achieve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Better Security:&lt;/strong&gt; Least-privilege access.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Cleaner Code:&lt;/strong&gt; Your storefront API doesn't need "Inventory Management" logic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Scalability:&lt;/strong&gt; You can spin up new microservices (e.g., a "Driver App" for delivery) and just hook them into the existing Auth ecosystem.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>storefront</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Architecting a B2B SaaS: Multi-Tenancy Made Simple</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Thu, 11 Dec 2025 10:11:49 +0000</pubDate>
      <link>https://dev.to/edgi/architecting-a-b2b-saas-multi-tenancy-made-simple-46ij</link>
      <guid>https://dev.to/edgi/architecting-a-b2b-saas-multi-tenancy-made-simple-46ij</guid>
      <description>&lt;p&gt;Building business software (SaaS) is different from consumer apps.&lt;/p&gt;

&lt;p&gt;You aren't just managing users; you are managing &lt;strong&gt;Teams&lt;/strong&gt; (or Organizations/Tenants).&lt;/p&gt;

&lt;p&gt;In this tutorial, we will build "TaskFlow," a Trello-like project management tool where:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Users&lt;/strong&gt; can belong to multiple &lt;strong&gt;Organizations&lt;/strong&gt; (e.g., "Work", "Side Project").&lt;/li&gt;
&lt;li&gt; Each Organization has its own &lt;strong&gt;Projects&lt;/strong&gt; and &lt;strong&gt;Billing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Data must be strictly isolated (Tenant A cannot see Tenant B's data).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 1: The Multi-Tenant Data Model
&lt;/h2&gt;

&lt;p&gt;The database schema is the foundation of multi-tenancy. &lt;br&gt;
Every resource must "belong" to an organization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// schema.prisma

model Organization {
  id        String   @id @default(uuid())
  name      String
  slug      String   @unique // e.g., taskflow.com/acme-corp
  members   OrgMember[]
  projects  Project[]
}

model OrgMember {
  id        String   @id @default(uuid())
  orgId     String
  userId    String   // The Rugi Auth User ID
  role      String   // 'OWNER', 'MEMBER', 'GUEST'

  organization Organization @relation(fields: [orgId], references: [id])

  @@unique([orgId, userId]) // User can join an org only once
}

model Project {
  id        String   @id @default(uuid())
  orgId     String
  title     String
  tasks     Task[]

  // CRITICAL: Linking project to Org ensures isolation
  organization Organization @relation(fields: [orgId], references: [id])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 2: Resolving Identity vs. Context
&lt;/h2&gt;

&lt;p&gt;Here is the conceptual leap:&lt;br&gt;
&lt;strong&gt;Identity&lt;/strong&gt; is "Who are you?" (I am Alice).&lt;br&gt;
&lt;strong&gt;Context&lt;/strong&gt; is "Where are you?" (I am currently working in Acme Corp).&lt;/p&gt;

&lt;p&gt;Many developers make the mistake of creating a new user account for every organization (&lt;code&gt;alice@acme.com&lt;/code&gt;, &lt;code&gt;alice@beta.com&lt;/code&gt;). This is a nightmare for users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Better Way (Powered by Rugi Auth):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Alice has &lt;strong&gt;One Identity&lt;/strong&gt; in Rugi Auth.&lt;/li&gt;
&lt;li&gt; Your application handles the &lt;strong&gt;Context&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 1: Authentication (Identity)
&lt;/h3&gt;

&lt;p&gt;Alice logs in via Rugi Auth. We get her &lt;code&gt;userId&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Tenant Resolution (Context)
&lt;/h3&gt;

&lt;p&gt;When Alice visits &lt;code&gt;app.taskflow.com/acme-corp/dashboard&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Middleware Check:&lt;/strong&gt;
Is Alice authenticated? (Yes, valid Token).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Membership Check:&lt;/strong&gt;
Does &lt;code&gt;OrgMember&lt;/code&gt; table have an entry for &lt;code&gt;userId: Alice&lt;/code&gt; AND &lt;code&gt;orgId: AcmeCorp&lt;/code&gt;?

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Yes:&lt;/strong&gt; Proceed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No:&lt;/strong&gt; 403 Forbidden.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware/tenant.ts&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;prisma&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;./db&lt;/span&gt;&lt;span class="dl"&gt;'&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;requireTenantMembership&lt;/span&gt; &lt;span class="o"&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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;id&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;orgSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgSlug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// from URL&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgMember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orgSlug&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="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;member&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are not a member of this organization&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="c1"&gt;// Attach membership info to request for downstream use&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;member&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;h2&gt;
  
  
  Phase 3: Invitation System (Growth Engine)
&lt;/h2&gt;

&lt;p&gt;SaaS grows via invites. "Alice invites Bob to Acme Corp."&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Frontend:&lt;/strong&gt; Alice enters Bob's email in the dashboard.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Backend:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Check if Bob already has a Rugi Auth account?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;If Yes:&lt;/strong&gt; Add entry to &lt;code&gt;OrgMember&lt;/code&gt;. Send email "You've been added!".&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;If No:&lt;/strong&gt; create a "Pending Invite".&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Using Rugi Auth's Registration API:&lt;/strong&gt;&lt;br&gt;
You can pre-provision users or simply send them to your registration page.&lt;br&gt;
When Bob finally registers with that email, your webhook (or post-registration hook) detects the pending invite and automatically adds him to the organization.&lt;/p&gt;


&lt;h2&gt;
  
  
  Phase 4: Billing &amp;amp; Admin (Global Scope)
&lt;/h2&gt;

&lt;p&gt;You, as the SaaS builder, need a "God Mode" to see who is paying.&lt;/p&gt;

&lt;p&gt;In Rugi Auth, define a role &lt;code&gt;SAAS_SUPER_ADMIN&lt;/code&gt; in your app configuration.&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="c1"&gt;// routes/billing.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Only for YOU&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/all-tenants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SAAS_SUPER_ADMIN&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;allOrgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allOrgs&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;This keeps your "Super Admin" logic completely separate from the "Organization Owner" logic, preventing the accidental "Customer A sees Customer B" bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Building a SaaS is about rigorous data isolation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Rugi Auth&lt;/strong&gt; handles the global "Who is this person?" question.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Your App&lt;/strong&gt; handles the huge "Which team are they on?" matrix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation allows your SaaS to support users who belong to 50 different teams with a single login, a feature users love and expect from modern tools.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>b2b</category>
    </item>
    <item>
      <title>Modernizing Enterprise IT: The Unified Internal Portal</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Thu, 11 Dec 2025 10:04:15 +0000</pubDate>
      <link>https://dev.to/edgi/modernizing-enterprise-it-the-unified-internal-portal-37gc</link>
      <guid>https://dev.to/edgi/modernizing-enterprise-it-the-unified-internal-portal-37gc</guid>
      <description>&lt;p&gt;Every growing company eventually ends up with "&lt;a href="https://www.dynatrace.com/knowledge-base/tool-sprawl/" rel="noopener noreferrer"&gt;The Tool Sprawl.&lt;/a&gt;"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Utilization: "Log into the HR portal for leave."&lt;/li&gt;
&lt;li&gt;  Finance: "Log into the Payroll system (different password)."&lt;/li&gt;
&lt;li&gt;  Engineering: "Log into the Jenkins dashboard."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Employees are frustrated, and IT is overwhelmed with password resets. &lt;br&gt;
The solution is &lt;strong&gt;Single Sign-On (SSO)&lt;/strong&gt;, usually via an expensive enterprise vendor. Today, we will build our own using Rugi Auth.&lt;/p&gt;

&lt;p&gt;We will simulate a fragmented corporate environment and unify it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Components
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The "Legacy" System:&lt;/strong&gt; A PHP-based employee directory (simulated).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Modern" System:&lt;/strong&gt; A Node.js Analytics Dashboard.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Identity Provider (IdP):&lt;/strong&gt; Rugi Auth hosted on &lt;code&gt;auth.internal.corp&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Step 1: Setting up the Identity Provider
&lt;/h2&gt;

&lt;p&gt;We deploy Rugi Auth internally. We create two Apps in the dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Legacy Directory&lt;/strong&gt; (Client ID: &lt;code&gt;legacy-app&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analytics Dash&lt;/strong&gt; (Client ID: &lt;code&gt;analytics-app&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also enable &lt;strong&gt;LDAP&lt;/strong&gt; or just use the database to store employee credentials centrally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: The "Portal" (The Launchpad)
&lt;/h2&gt;

&lt;p&gt;We build a simple static HTML page or a lightweight React app that serves as the "Dock" for employees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- portal.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome, Employee&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"apps"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"http://hr.internal.corp/login?sso=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Go to HR Directory&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"http://analytics.internal.corp/login?sso=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Go to Analytics&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;// Check if we have a valid Rugi Auth session&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkSession&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;user&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://auth.internal.corp/me&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Hello &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="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://auth.internal.corp/login&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;checkSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Integrating the "Modern" Node.js App
&lt;/h2&gt;

&lt;p&gt;This is easy. The Analytics app uses standard JWT validation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Login Flow:&lt;/strong&gt;&lt;br&gt;
The user clicks "Login with SSO". We redirect them to Rugi Auth.&lt;br&gt;
&lt;code&gt;http://auth.internal.corp/login?client_id=analytics-app&amp;amp;redirect_uri=...&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Callback:&lt;/strong&gt;&lt;br&gt;
Rugi Auth validates the user (who is &lt;em&gt;already&lt;/em&gt; logged in from the Portal!) and immediately redirects back with a code/token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token Validation:&lt;/strong&gt;&lt;br&gt;
The Node app validates the token against Rugi Auth's public keys (JWKS).&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Standard JWT verification using jwks-rsa&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jwksClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jwksUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://auth.internal.corp/.well-known/jwks.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// ... verify token signature ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Integrating the "Legacy" PHP App
&lt;/h2&gt;

&lt;p&gt;This is often the scary part. &lt;/p&gt;

&lt;p&gt;How do you teach a 10-year-old PHP script to understand modern Auth?&lt;/p&gt;

&lt;p&gt;You don't need to rewrite the app, You just need a &lt;strong&gt;Bridge Script&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PHP Logic (&lt;code&gt;login.php&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Capture the ID Token sent from the redirect&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'access_token'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Verify it (Simpler method: call the introspection endpoint)&lt;/span&gt;
&lt;span class="c1"&gt;// In a real high-traffic app, you'd verify the signature locally in PHP.&lt;/span&gt;
&lt;span class="c1"&gt;// For internal tools, asking Rugi Auth "Is this valid?" is fine.&lt;/span&gt;

&lt;span class="nv"&gt;$ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http://auth.internal.corp/me'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;curl_setopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CURLOPT_HTTPHEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Authorization: Bearer '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&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="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SUCCESS!&lt;/span&gt;
    &lt;span class="c1"&gt;// Set the LEGACY session variable that the old app expects&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_logged_in'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /dashboard.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SSO Failed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By adding this one &lt;code&gt;login.php&lt;/code&gt; file, you have effectively "modernized" the authentication of a legacy artifact without touching its spaghetti core code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: The Employee Experience
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Morning:&lt;/strong&gt; Employee logs into the &lt;strong&gt;Portal&lt;/strong&gt;. (Enters password once).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mid-day:&lt;/strong&gt; Opens &lt;strong&gt;Analytics&lt;/strong&gt;. 
The app detects no session, redirects to Rugi Auth. 
Rugi Auth sees the cookie from the morning, and &lt;em&gt;immediately&lt;/em&gt; redirects back with a token. &lt;strong&gt;Zero typing.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Afternoon:&lt;/strong&gt; Opens &lt;strong&gt;Legacy HR&lt;/strong&gt;. Same thing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In an enterprise environment, "Full Stack" often means gluing together different generations of technology.&lt;br&gt;
Rugi Auth serves as the &lt;strong&gt;Universal Adapter&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It speaks &lt;strong&gt;JWT/OIDC&lt;/strong&gt; for your modern React/Node apps.&lt;/li&gt;
&lt;li&gt;  It provides simple &lt;strong&gt;REST endpoints&lt;/strong&gt; that even a bash script or PHP 5 app can consume to verify identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This centralization creates a single point of control. &lt;br&gt;
When an employee leaves, you disable them in Rugi Auth, and they are instantly locked out of the Portal, the Analytics, AND the Legacy HR system.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>tutorial</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Building Secure Multi-App Authentication with rugi-auth: A Complete Guide</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Mon, 08 Dec 2025 11:20:14 +0000</pubDate>
      <link>https://dev.to/edgi/building-secure-multi-app-authentication-with-rugi-auth-a-complete-guide-39mm</link>
      <guid>https://dev.to/edgi/building-secure-multi-app-authentication-with-rugi-auth-a-complete-guide-39mm</guid>
      <description>&lt;p&gt;&lt;em&gt;Centralized authentication that scales across your entire application ecosystem&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today's microservices and multi-application architectures, managing authentication can become a nightmare. &lt;br&gt;
Each service needs to verify users, handle tokens, manage roles, and maintain security-often duplicating logic across multiple codebases. What if you could centralize all of this into a single, secure, battle-tested authentication service?&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;rugi-auth&lt;/strong&gt;: a production-ready, centralized authentication service built with TypeScript, Express, and Prisma. &lt;br&gt;
It provides enterprise-grade security features while remaining simple to integrate and deploy.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is rugi-auth?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; is a secure, centralized authentication service designed for modern application architectures. &lt;br&gt;
It enables a single user identity to work seamlessly across multiple applications while maintaining granular, app-specific role management. &lt;br&gt;
Think of it as your own Auth0 or Okta, but open-source and fully customizable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Core Philosophy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Identity&lt;/strong&gt;: One user account works across all your applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security First&lt;/strong&gt;: Built with industry-standard security practices from day one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Friendly&lt;/strong&gt;: Simple integration, comprehensive documentation, and TypeScript support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Ready&lt;/strong&gt;: Battle-tested features like rate limiting, audit logging, and token rotation&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Multi-App Architecture
&lt;/h3&gt;

&lt;p&gt;Unlike traditional auth systems that tie users to a single application, rugi-auth supports multiple applications (clients) with a shared user base. Each user can have different roles in different applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: john@example.com
├── E-commerce App    → roles: ['customer', 'reviewer']
├── Admin Dashboard   → roles: ['admin']
└── Content Platform  → roles: ['author', 'editor']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS platforms with multiple products&lt;/li&gt;
&lt;li&gt;Microservices architectures&lt;/li&gt;
&lt;li&gt;Organizations with separate internal and customer-facing apps&lt;/li&gt;
&lt;li&gt;White-label solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Multiple Authentication Methods
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; supports various authentication methods, all configurable per application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email + Password&lt;/strong&gt;: Traditional authentication with secure password hashing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email + OTP&lt;/strong&gt;: Passwordless authentication via one-time passwords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth Providers&lt;/strong&gt;: Google, GitHub, Microsoft, and Facebook (with more coming)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Methods&lt;/strong&gt;: Extensible architecture for adding new providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each application can enable or disable specific authentication methods based on their needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. App-Specific Role Management
&lt;/h3&gt;

&lt;p&gt;Roles are scoped to applications, meaning a user can be an "admin" in one app and a "user" in another. &lt;/p&gt;

&lt;p&gt;This provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Granular Permissions&lt;/strong&gt;: Fine-grained access control per application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Isolation&lt;/strong&gt;: Roles in one app don't affect another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Management&lt;/strong&gt;: Assign roles programmatically or via API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. JWT with RS256 and JWKS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; uses industry-standard JWT tokens signed with RSA-256:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RS256 Algorithm&lt;/strong&gt;: Asymmetric encryption ensures tokens can be verified without exposing secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWKS Endpoint&lt;/strong&gt;: Public keys are exposed via &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; for easy token verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Rotation&lt;/strong&gt;: Support for key rotation without service downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless Verification&lt;/strong&gt;: Consumer applications verify tokens without database lookups&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Refresh Token Rotation
&lt;/h3&gt;

&lt;p&gt;For enhanced security, refresh tokens are rotated on every use:&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="c1"&gt;// First refresh&lt;/span&gt;
&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;old-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new-token-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Second refresh (old token is now invalid)&lt;/span&gt;
&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new-token-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new-token-2&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;This prevents token reuse attacks and ensures compromised tokens become useless quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Comprehensive Audit Logging
&lt;/h3&gt;

&lt;p&gt;Every authentication event is logged for security auditing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User registrations&lt;/li&gt;
&lt;li&gt;Login attempts (successful and failed)&lt;/li&gt;
&lt;li&gt;Token refreshes&lt;/li&gt;
&lt;li&gt;Role assignments&lt;/li&gt;
&lt;li&gt;Password reset requests&lt;/li&gt;
&lt;li&gt;OTP requests and usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This provides complete visibility into authentication activities for compliance and security monitoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Microservices Architecture
&lt;/h3&gt;

&lt;p&gt;In a microservices setup, each service needs to verify user identity. With &lt;strong&gt;rugi-auth&lt;/strong&gt;, services can verify JWT tokens independently without sharing databases or secrets:&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="c1"&gt;// In your microservice&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;createAuthMiddleware&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;rugi-auth/client&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jwksUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://auth.example.com/.well-known/jwks.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rugi-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-service-id&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// req.user is automatically populated&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOrdersForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;orders&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;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No shared database between services&lt;/li&gt;
&lt;li&gt;Stateless token verification&lt;/li&gt;
&lt;li&gt;Independent service scaling&lt;/li&gt;
&lt;li&gt;Centralized user management&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Multi-Product SaaS Platform
&lt;/h3&gt;

&lt;p&gt;If you're building a SaaS platform with multiple products (e.g., a CRM, an analytics dashboard, and a marketing tool), &lt;strong&gt;rugi-auth&lt;/strong&gt; enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single sign-on across all products&lt;/li&gt;
&lt;li&gt;Product-specific roles and permissions&lt;/li&gt;
&lt;li&gt;Unified user management&lt;/li&gt;
&lt;li&gt;Centralized billing and subscription tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. White-Label Solutions
&lt;/h3&gt;

&lt;p&gt;For white-label applications where you need to support multiple client organizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each client gets their own application instance&lt;/li&gt;
&lt;li&gt;Users can have different roles per client&lt;/li&gt;
&lt;li&gt;Centralized authentication reduces operational overhead&lt;/li&gt;
&lt;li&gt;Easy onboarding of new clients&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Enterprise Applications
&lt;/h3&gt;

&lt;p&gt;Large organizations often have multiple internal tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HR systems&lt;/li&gt;
&lt;li&gt;Project management tools&lt;/li&gt;
&lt;li&gt;Internal dashboards&lt;/li&gt;
&lt;li&gt;Customer-facing applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; provides a single identity provider for all these systems while maintaining appropriate access controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. API Gateway Authentication
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;rugi-auth&lt;/strong&gt; as the authentication layer for your API gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All API requests are authenticated via JWT&lt;/li&gt;
&lt;li&gt;Gateway verifies tokens using JWKS&lt;/li&gt;
&lt;li&gt;Backend services receive validated user context&lt;/li&gt;
&lt;li&gt;Centralized token management&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reduced Development Time
&lt;/h3&gt;

&lt;p&gt;Instead of implementing authentication in every application, you set it up once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No repeated code&lt;/strong&gt;: Authentication logic lives in one place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster feature development&lt;/strong&gt;: Focus on business logic, not auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent behavior&lt;/strong&gt;: All apps use the same authentication flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy updates&lt;/strong&gt;: Security improvements benefit all applications&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Enhanced Security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; implements security best practices out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id Password Hashing&lt;/strong&gt;: Memory-hard algorithm resistant to GPU attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting&lt;/strong&gt;: Redis-backed distributed rate limiting prevents brute force attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing Attack Protection&lt;/strong&gt;: Constant-time operations prevent user enumeration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Rotation&lt;/strong&gt;: Refresh tokens rotate on use, limiting attack windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Logging&lt;/strong&gt;: Complete visibility into authentication events&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Scalability
&lt;/h3&gt;

&lt;p&gt;Designed for scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateless Tokens&lt;/strong&gt;: No database lookups for token verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Rate Limiting&lt;/strong&gt;: Distributed rate limiting across multiple instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scaling&lt;/strong&gt;: Run multiple instances behind a load balancer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Optimization&lt;/strong&gt;: Efficient queries with proper indexing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Developer Experience
&lt;/h3&gt;

&lt;p&gt;Built with developers in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript Support&lt;/strong&gt;: Full type safety and IntelliSense&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight Client&lt;/strong&gt;: Consumer apps only need 2 dependencies (&lt;code&gt;jsonwebtoken&lt;/code&gt;, &lt;code&gt;jwks-rsa&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive Documentation&lt;/strong&gt;: Integration guides, API references, and examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI Tools&lt;/strong&gt;: Scaffold projects, generate keys, create superadmins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swagger UI&lt;/strong&gt;: Interactive API documentation at &lt;code&gt;/docs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Flexibility
&lt;/h3&gt;

&lt;p&gt;Highly configurable to fit your needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-App Settings&lt;/strong&gt;: Enable/disable authentication methods per application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Roles&lt;/strong&gt;: Define roles specific to each application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Configuration&lt;/strong&gt;: Customize email templates and SMTP settings per app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth Providers&lt;/strong&gt;: Enable Google, GitHub, Microsoft, or Facebook per app&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Cost Efficiency
&lt;/h3&gt;

&lt;p&gt;Open-source alternative to commercial solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Per-User Fees&lt;/strong&gt;: Unlike Auth0 or Okta, no cost per user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted&lt;/strong&gt;: Full control over infrastructure and data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Vendor Lock-in&lt;/strong&gt;: Your data, your rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable&lt;/strong&gt;: Modify to fit your exact requirements&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Security Measures
&lt;/h2&gt;

&lt;p&gt;Security is at the core of &lt;strong&gt;rugi-auth&lt;/strong&gt;. Here's a deep dive into the security measures implemented:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Password Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Argon2id Hashing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; uses Argon2id, the winner of the Password Hashing Competition, for password hashing:&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="c1"&gt;// Configuration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ARGON2_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;argon2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argon2id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Hybrid approach (resistant to both side-channel and GPU attacks)&lt;/span&gt;
  &lt;span class="na"&gt;memoryCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// 64MB memory&lt;/span&gt;
  &lt;span class="na"&gt;timeCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// 3 iterations&lt;/span&gt;
  &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// 4 threads&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Argon2id?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory-Hard&lt;/strong&gt;: Requires significant memory, making GPU attacks expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side-Channel Resistant&lt;/strong&gt;: Constant-time operations prevent timing attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunable&lt;/strong&gt;: Adjust memory, time, and parallelism based on your security needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Timing Attack Protection
&lt;/h4&gt;

&lt;p&gt;The login flow is designed to prevent timing attacks that could reveal whether a user exists:&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="c1"&gt;// Both paths take similar time&lt;/span&gt;
&lt;span class="c1"&gt;// Non-existent user: hash verification attempt (constant time)&lt;/span&gt;
&lt;span class="c1"&gt;// Wrong password: hash verification attempt (constant time)&lt;/span&gt;
&lt;span class="c1"&gt;// Result: Attacker can't distinguish between "user doesn't exist" and "wrong password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is tested to ensure execution times are within 50-100ms of each other, regardless of whether the user exists or the password is wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Token Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  RS256 Algorithm
&lt;/h4&gt;

&lt;p&gt;Tokens are signed using RS256 (RSA with SHA-256), an asymmetric algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private Key&lt;/strong&gt;: Used by auth service to sign tokens (never exposed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public Key&lt;/strong&gt;: Used by consumer apps to verify tokens (exposed via JWKS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benefits&lt;/strong&gt;: Consumer apps can verify tokens without knowing the secret&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  JWKS (JSON Web Key Set)
&lt;/h4&gt;

&lt;p&gt;Public keys are exposed via a standard JWKS endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "abc123",
      "use": "sig",
      "n": "...",
      "e": "AQAB"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows OAuth 2.0 and OpenID Connect standards, making integration with standard libraries straightforward.&lt;/p&gt;

&lt;h4&gt;
  
  
  Token Claims
&lt;/h4&gt;

&lt;p&gt;Tokens include standard JWT claims plus custom claims:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Subject (user ID)&lt;/span&gt;
  &lt;span class="nx"&gt;aud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;client-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Audience (application client ID)&lt;/span&gt;
  &lt;span class="nx"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Tenant ID (application ID)&lt;/span&gt;
  &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// User roles for this app&lt;/span&gt;
  &lt;span class="nx"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rugi-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Issuer&lt;/span&gt;
  &lt;span class="nx"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// Issued at&lt;/span&gt;
  &lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1234567890&lt;/span&gt;           &lt;span class="c1"&gt;// Expires at&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Refresh Token Rotation
&lt;/h4&gt;

&lt;p&gt;Refresh tokens rotate on every use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client uses refresh token to get new access token&lt;/li&gt;
&lt;li&gt;Server generates new refresh token&lt;/li&gt;
&lt;li&gt;Old refresh token is immediately invalidated&lt;/li&gt;
&lt;li&gt;Client must save the new refresh token&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures that if a refresh token is compromised, it can only be used once before becoming invalid.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rate Limiting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; implements Redis-backed distributed rate limiting:&lt;/p&gt;

&lt;h4&gt;
  
  
  Authentication Endpoints
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Standard rate limiter&lt;/span&gt;
&lt;span class="nx"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt;
&lt;span class="nx"&gt;maxRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="nx"&gt;per&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Sensitive Operations
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stricter rate limiter for password reset, OTP requests&lt;/span&gt;
&lt;span class="nx"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt;
&lt;span class="nx"&gt;maxRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="nx"&gt;per&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distributed&lt;/strong&gt;: Works across multiple server instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis-Backed&lt;/strong&gt;: Persistent rate limiting across restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP-Based&lt;/strong&gt;: Uses &lt;code&gt;req.ip&lt;/code&gt; (respects &lt;code&gt;trust proxy&lt;/code&gt; setting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Fallback&lt;/strong&gt;: Falls back to memory store if Redis is unavailable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Input Validation
&lt;/h3&gt;

&lt;p&gt;All inputs are validated using Joi schemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email Validation&lt;/strong&gt;: RFC-compliant email validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password Strength&lt;/strong&gt;: Configurable password requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Credentials&lt;/strong&gt;: Validates client_id and client_secret format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Validation&lt;/strong&gt;: Validates token structure before processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. SQL Injection Protection
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; uses Prisma ORM, which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameterized Queries&lt;/strong&gt;: All queries use parameterized statements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt;: TypeScript types prevent invalid queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Injection Prevention&lt;/strong&gt;: Built-in protection against SQL injection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. CORS and Security Headers
&lt;/h3&gt;

&lt;p&gt;Configurable CORS and security headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CORS&lt;/strong&gt;: Configurable allowed origins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helmet.js&lt;/strong&gt;: Security headers (X-Content-Type-Options, X-Frame-Options, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS Enforcement&lt;/strong&gt;: Can enforce HTTPS in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Audit Logging
&lt;/h3&gt;

&lt;p&gt;Comprehensive audit logging for security monitoring:&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="c1"&gt;// All events are logged&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;LOGIN &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;successful&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;REGISTER&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;REFRESH&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;REVOKE&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ROLE_ASSIGN&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;PASSWORD_RESET_REQUEST&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;PASSWORD_RESET_COMPLETE&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;OTP_REQUEST&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;OTP_LOGIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each log entry includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User ID (if applicable)&lt;/li&gt;
&lt;li&gt;Action type&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Metadata (IP address, user agent, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Client Secret Security
&lt;/h3&gt;

&lt;p&gt;For confidential applications, client secrets are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hashed&lt;/strong&gt;: Stored as Argon2id hashes (not plaintext)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validated&lt;/strong&gt;: Verified on every authentication request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotatable&lt;/strong&gt;: Can be rotated without affecting existing tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  9. Email Security
&lt;/h3&gt;

&lt;p&gt;OTP and password reset emails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-Limited&lt;/strong&gt;: Codes expire after a set duration (default: 10 minutes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-Use&lt;/strong&gt;: OTP codes can only be used once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limited&lt;/strong&gt;: Prevents email spam and brute force attacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10. Database Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indexed Queries&lt;/strong&gt;: Efficient queries prevent timing-based enumeration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascade Deletes&lt;/strong&gt;: Proper cleanup when users or apps are deleted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Safety&lt;/strong&gt;: Critical operations use database transactions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quick Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; can be set up in minutes:&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="c"&gt;# 1. Initialize a new project&lt;/span&gt;
npx rugi-auth init my-auth-service

&lt;span class="c"&gt;# 2. Start infrastructure&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-auth-service
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# 3. Run migrations and setup&lt;/span&gt;
npm run prisma:migrate
npm run setup

&lt;span class="c"&gt;# 4. Start the server&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your authentication service is now running at &lt;code&gt;http://localhost:7100&lt;/code&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration Example
&lt;/h3&gt;

&lt;p&gt;Integrating with your Express application 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="c1"&gt;// Install the lightweight client&lt;/span&gt;
&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;rugi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;

&lt;span class="c1"&gt;// In your Express app&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;createAuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireRole&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;rugi-auth/client&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jwksUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://auth.example.com/.well-known/jwks.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rugi-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-client-id&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="c1"&gt;// Protect routes&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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;roles&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="c1"&gt;// Role-based access control&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/admin&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Admin access granted&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User Registration/Login&lt;/strong&gt;: User authenticates with rugi-auth service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Issuance&lt;/strong&gt;: Service issues JWT access token and refresh token&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Requests&lt;/strong&gt;: Client includes access token in &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Verification&lt;/strong&gt;: Your app verifies token using JWKS (no database lookup needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Refresh&lt;/strong&gt;: When access token expires, use refresh token to get new tokens&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Standalone Service (Recommended)
&lt;/h3&gt;

&lt;p&gt;Deploy &lt;strong&gt;rugi-auth&lt;/strong&gt; as a separate service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│   Client    │      │  Your Apps   │      │  rugi-auth  │
│  (Browser)  │      │  (Express)   │      │   Service   │
└──────┬──────┘      └──────┬───────┘      └──────┬──────┘
       │                    │                      │
       │ 1. Login            │                      │
       │──────────────────────────────────────────&amp;gt;│
       │                    │                      │
       │ 2. JWT Tokens       │                      │
       │&amp;lt;──────────────────────────────────────────│
       │                    │                      │
       │ 3. API Request     │                      │
       │    (Bearer token)  │                      │
       │───────────────────&amp;gt;│                      │
       │                    │                      │
       │                    │ 4. Verify via JWKS  │
       │                    │─────────────────────&amp;gt;│
       │                    │                      │
       │                    │ 5. Public Key        │
       │                    │&amp;lt;─────────────────────│
       │                    │                      │
       │ 6. Response        │                      │
       │&amp;lt;───────────────────│                      │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microservices can verify tokens independently&lt;/li&gt;
&lt;li&gt;No shared database between services&lt;/li&gt;
&lt;li&gt;Centralized user management&lt;/li&gt;
&lt;li&gt;Easy to scale&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Package Import (Monolith)
&lt;/h3&gt;

&lt;p&gt;For monolithic applications, import &lt;strong&gt;rugi-auth&lt;/strong&gt; directly:&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;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roleMiddleware&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;rugi-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/protected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// req.user is available&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Real-World Example: E-Commerce Platform
&lt;/h2&gt;

&lt;p&gt;Imagine you're building an e-commerce platform with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer App&lt;/strong&gt;: Public-facing store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard&lt;/strong&gt;: Internal management tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor Portal&lt;/strong&gt;: For third-party sellers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics Service&lt;/strong&gt;: Separate microservice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;strong&gt;rugi-auth&lt;/strong&gt;, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single Sign-On&lt;/strong&gt;: Users log in once, access all apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-Based Access&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;Customers have &lt;code&gt;customer&lt;/code&gt; role in customer app&lt;/li&gt;
&lt;li&gt;Admins have &lt;code&gt;admin&lt;/code&gt; role in admin dashboard&lt;/li&gt;
&lt;li&gt;Vendors have &lt;code&gt;vendor&lt;/code&gt; role in vendor portal&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservice Authentication&lt;/strong&gt;: Analytics service verifies tokens via JWKS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Management&lt;/strong&gt;: Manage all users from one place&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Comparison with Alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;rugi-auth&lt;/th&gt;
&lt;th&gt;Auth0&lt;/th&gt;
&lt;th&gt;Okta&lt;/th&gt;
&lt;th&gt;Firebase Auth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free (self-hosted)&lt;/td&gt;
&lt;td&gt;Per-user pricing&lt;/td&gt;
&lt;td&gt;Per-user pricing&lt;/td&gt;
&lt;td&gt;Free tier, then per-user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-Hosted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-App Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App-Specific Roles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;⚠️ Complex&lt;/td&gt;
&lt;td&gt;⚠️ Complex&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;⚠️ SDK only&lt;/td&gt;
&lt;td&gt;⚠️ SDK only&lt;/td&gt;
&lt;td&gt;⚠️ SDK only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Token Storage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Client-Side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;httpOnly&lt;/code&gt; cookies when possible (prevents XSS)&lt;/li&gt;
&lt;li&gt;If using localStorage, ensure HTTPS only&lt;/li&gt;
&lt;li&gt;Never log tokens to console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server-Side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store refresh tokens securely (encrypted database)&lt;/li&gt;
&lt;li&gt;Implement token rotation&lt;/li&gt;
&lt;li&gt;Monitor for token abuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Configure appropriate rate limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: 100 requests per 15 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password Reset&lt;/strong&gt;: 3 requests per 15 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTP Requests&lt;/strong&gt;: 5 requests per 15 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Monitoring
&lt;/h3&gt;

&lt;p&gt;Monitor authentication events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failed login attempts&lt;/li&gt;
&lt;li&gt;Token refresh patterns&lt;/li&gt;
&lt;li&gt;Role assignment changes&lt;/li&gt;
&lt;li&gt;Unusual access patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Key Rotation
&lt;/h3&gt;

&lt;p&gt;Rotate RSA keys periodically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate new key pair&lt;/li&gt;
&lt;li&gt;Update JWKS endpoint&lt;/li&gt;
&lt;li&gt;Old tokens remain valid until expiry&lt;/li&gt;
&lt;li&gt;New tokens use new key&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. HTTPS
&lt;/h3&gt;

&lt;p&gt;Always use HTTPS in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protects tokens in transit&lt;/li&gt;
&lt;li&gt;Prevents man-in-the-middle attacks&lt;/li&gt;
&lt;li&gt;Required for secure cookies&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;rugi-auth&lt;/strong&gt; provides a robust, secure, and developer-friendly solution for centralized authentication. Whether you're building microservices, a multi-product SaaS platform, or an enterprise application suite, &lt;strong&gt;rugi-auth&lt;/strong&gt; gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Security&lt;/strong&gt;: Industry-standard practices out of the box&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Scalability&lt;/strong&gt;: Designed to handle growth&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Flexibility&lt;/strong&gt;: Configurable for your specific needs&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Developer Experience&lt;/strong&gt;: TypeScript, comprehensive docs, easy integration&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cost Efficiency&lt;/strong&gt;: Open-source, self-hosted, no per-user fees&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/EDMONDGIHOZO/rugi-auth" rel="noopener noreferrer"&gt;https://github.com/EDMONDGIHOZO/rugi-auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: See &lt;code&gt;/docs&lt;/code&gt; directory in the repository&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples&lt;/strong&gt;: Check &lt;code&gt;/examples&lt;/code&gt; for integration examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Join the Community
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Report issues, suggest features, or contribute on GitHub&lt;/li&gt;
&lt;li&gt;Share your integration stories and use cases&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Ready to centralize your authentication? Give **rugi-auth&lt;/em&gt;* a try and experience the benefits of secure, scalable, multi-app authentication.*&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article was written to help developers understand the power and capabilities of &lt;strong&gt;rugi-auth&lt;/strong&gt;, a production-ready authentication service built for modern application architectures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keywords&lt;/strong&gt;: Authentication, JWT, OAuth, Microservices, TypeScript, Express, Security, Multi-Tenant, RBAC, Centralized Auth&lt;/p&gt;

</description>
      <category>centralizedauth</category>
      <category>authentication</category>
      <category>multitenant</category>
      <category>jwt</category>
    </item>
    <item>
      <title>The Hidden Vulnerabilities in Your Authentication System: A Deep Dive into Timing Attacks, IP Spoofing, and Race Conditions</title>
      <dc:creator>edmondgi</dc:creator>
      <pubDate>Sun, 07 Dec 2025 20:24:12 +0000</pubDate>
      <link>https://dev.to/edgi/the-hidden-vulnerabilities-in-your-authentication-system-a-deep-dive-into-timing-attacks-ip-5k9</link>
      <guid>https://dev.to/edgi/the-hidden-vulnerabilities-in-your-authentication-system-a-deep-dive-into-timing-attacks-ip-5k9</guid>
      <description>&lt;p&gt;&lt;em&gt;How we discovered and fixed critical security flaws that most authentication systems overlook&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
Authentication is the foundation of application security. &lt;/p&gt;

&lt;p&gt;Yet, many developers even experienced ones overlook subtle vulnerabilities that can expose user data, enable account enumeration, or allow attackers to bypass rate limiting. &lt;/p&gt;

&lt;p&gt;Over the past few months, I've been working on hardening an authentication service, and what I discovered shocked me: three critical vulnerabilities that are surprisingly common in production systems.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through these vulnerabilities, explain why they're dangerous, and show you how to fix them. Whether you're building your own auth system or evaluating third-party solutions, understanding these issues is crucial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Vulnerability #1: Timing Attacks on Password Verification&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
Imagine an attacker trying to guess a user's password. &lt;br&gt;
They send login requests with different passwords and measure the response time. &lt;/p&gt;

&lt;p&gt;If the server takes longer to respond when the username exists (even with a wrong password), the attacker knows the account exists. This is called a timing attack.&lt;/p&gt;

&lt;p&gt;Here's what was happening in our code:&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.amazonaws.com%2Fuploads%2Farticles%2F5iehr122j1svxtobqfd0.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.amazonaws.com%2Fuploads%2Farticles%2F5iehr122j1svxtobqfd0.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The vulnerability: When a user doesn't exist, the function returns immediately. When a user exists but the password is wrong, it performs the expensive password verification first. &lt;br&gt;
This timing difference leaks information.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;Account Enumeration: Attackers can determine which email addresses are registered&lt;br&gt;
User Privacy: Violates privacy by revealing account existence&lt;br&gt;
Targeted Attacks: Attackers can focus on known accounts&lt;br&gt;
&lt;strong&gt;The Fix&lt;/strong&gt;&lt;br&gt;
We implemented constant-time verification by always performing password verification, even for non-existent users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SECURE CODE
const user = await findUserByEmail(email);
const DUMMY_HASH = '$argon2id$v=19$m=65536,t=3,p=4$...'; // Pre-computed hash

// Always perform both verifications in parallel
const [, realResult] = await Promise.all([
  verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // Dummy verification (always fails)
  user &amp;amp;&amp;amp; user.passwordHash 
    ? verifyPassword(user.passwordHash, input.password)
    : verifyPassword(DUMMY_HASH, DUMMY_PASSWORD), // Dummy if no user
]);

if (!user || !user.passwordHash || !realResult) {
  throw new AuthError('Invalid credentials');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key improvements:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both paths take similar time (constant-time execution)&lt;br&gt;
No information leakage about user existence&lt;br&gt;
Parallel execution maintains performance&lt;br&gt;
Vulnerability #2: IP Spoofing in Rate Limiting&lt;br&gt;
The Problem&lt;br&gt;
Rate limiting is essential for preventing brute-force attacks. However, many implementations trust the X-Forwarded-For header directly, which can be easily spoofed by attackers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// VULNERABLE CODE
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
// Use IP for rate limiting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vulnerability: Attackers can send fake X-Forwarded-For headers to bypass rate limits or frame innocent users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It Matters&lt;/strong&gt;&lt;br&gt;
Bypass Rate Limits: Attackers can make unlimited requests&lt;br&gt;
IP Spoofing: Frame legitimate users by using their IPs&lt;br&gt;
DDoS Amplification: Distribute attacks across multiple fake IPs&lt;br&gt;
The Fix&lt;br&gt;
We fixed this by relying on Express's built-in req.ip, which respects the trust proxy setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SECURE CODE
// In app.ts - configure trust proxy properly
if (env.nodeEnv === 'production') {
  app.set('trust proxy', true); // Trust reverse proxy
} else if (process.env.TRUST_PROXY) {
  app.set('trust proxy', process.env.TRUST_PROXY);
}

// In rate limiter
keyGenerator: (req) =&amp;gt; {
  // req.ip respects 'trust proxy' setting
  // Express validates X-Forwarded-For when trust proxy is configured
  return req.ip || 'unknown';
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key improvements:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Express validates proxy headers when trust proxy is set&lt;br&gt;
No manual header parsing (which can be spoofed)&lt;br&gt;
Proper configuration for production deployments&lt;/p&gt;

&lt;p&gt;Vulnerability #3: Race Condition in OTP Verification&lt;br&gt;
&lt;strong&gt;The Problem&lt;/strong&gt;&lt;br&gt;
When verifying one-time passwords (OTPs), there's a critical window between checking if an OTP is valid and marking it as used. &lt;br&gt;
An attacker can exploit this with concurrent requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// VULNERABLE CODE (TOCTOU - Time of Check, Time of Use)
const otp = await findOTP(userId, code);

if (!otp || otp.used || otp.expiresAt &amp;lt; new Date()) {
  throw new AuthError('Invalid OTP');
}

// ⚠️ RACE CONDITION WINDOW: Another request could verify the same OTP here

await markOTPAsUsed(otp.id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The vulnerability:&lt;/strong&gt; Two simultaneous requests can both verify the same OTP before either marks it as used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It Matters&lt;/strong&gt;&lt;br&gt;
OTP Reuse: Attackers can use the same OTP multiple times&lt;br&gt;
Security Bypass: Defeats the purpose of one-time passwords&lt;br&gt;
Account Takeover: Multiple login sessions from a single OTP&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;&lt;br&gt;
We used an atomic database operation to verify and mark as used in a single transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SECURE CODE
const result = await prisma.emailOTP.updateMany({
  where: {
    userId: user.id,
    code,
    used: false,
    expiresAt: { gt: new Date() }, // Check expiry DB-side
  },
  data: {
    used: true, // Atomic update
  },
});

if (result.count === 0) {
  throw new AuthError('Invalid or expired OTP code');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key improvements:
&lt;/h2&gt;

&lt;p&gt;Atomic operation (verify + mark as used in one query)&lt;br&gt;
Database-level expiry check&lt;br&gt;
No race condition window&lt;br&gt;
Only one request can succeed per OTP&lt;br&gt;
Bonus: Distributed Rate Limiting with Redis&lt;br&gt;
While fixing these vulnerabilities, we also improved our rate limiting architecture by moving from in-memory to Redis-backed distributed rate limiting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Horizontal Scaling: Rate limits work across multiple server instances&lt;br&gt;
Consistency: Same limits apply regardless of which server handles the request&lt;br&gt;
Resilience: Graceful fallback to memory store if Redis is unavailable&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Redis-backed rate limiting with fallback
const redisClient = new Redis({
  host: env.redis.host,
  port: env.redis.port,
  password: env.redis.password,
  retryStrategy: (times) =&amp;gt; Math.min(times * 50, 3000),
});

redisClient.on('error', (error) =&amp;gt; {
  logger.warn('Redis connection error, falling back to memory store');
  // express-rate-limit automatically falls back to memory store
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Impact: Before vs. After&lt;br&gt;
Before&lt;br&gt;
❌ Timing attacks could enumerate user accounts&lt;br&gt;
❌ Rate limiting could be bypassed via IP spoofing&lt;br&gt;
❌ OTPs could be reused through race conditions&lt;br&gt;
❌ Rate limiting only worked per-server instance&lt;br&gt;
After&lt;br&gt;
✅ Constant-time password verification (no information leakage)&lt;br&gt;
✅ Proper IP handling with Express trust proxy&lt;br&gt;
✅ Atomic OTP verification (no race conditions)&lt;br&gt;
✅ Distributed rate limiting with Redis&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Practices for Authentication Security
&lt;/h2&gt;

&lt;p&gt;Based on our experience, here are the key principles every authentication system should follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Constant-Time Operations&lt;br&gt;
Always ensure security-critical operations take the same time regardless of input. Use dummy operations when needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never Trust Client Headers Directly&lt;br&gt;
Always validate proxy headers through your framework's built-in mechanisms (like Express's trust proxy).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use Atomic Database Operations&lt;br&gt;
For operations that check and modify state (like OTP verification), use atomic database operations to prevent race conditions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Distributed Rate Limiting&lt;br&gt;
Use Redis or similar for rate limiting in distributed systems. &lt;br&gt;
Always have a fallback mechanism.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comprehensive Testing&lt;br&gt;
Write security-focused tests that specifically check for timing attacks, race conditions, and edge cases.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Testing Your Authentication System&lt;br&gt;
We implemented comprehensive security tests to ensure these vulnerabilities stay fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe('Timing Attack Protection', () =&amp;gt; {
  it('should have constant-time execution for non-existent users', async () =&amp;gt; {
    const time1 = await measureLoginTime('nonexistent1@example.com');
    const time2 = await measureLoginTime('nonexistent2@example.com');

    // Times should be similar (within 50ms tolerance)
    expect(Math.abs(time1 - time2)).toBeLessThan(50);
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key test categories:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Timing attack protection&lt;br&gt;
Rate limiting effectiveness&lt;br&gt;
Race condition prevention&lt;br&gt;
Input validation&lt;br&gt;
Conclusion: Building Secure Authentication&lt;br&gt;
Authentication security is a continuous process. &lt;br&gt;
The vulnerabilities we discovered are subtle but critical, and they're more common than you might think. By understanding these issues and implementing proper fixes, you can significantly improve your application's security posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;br&gt;
Timing attacks are real - Always use constant-time operations for security-critical code&lt;br&gt;
Never trust client headers - Use framework mechanisms for proxy validation&lt;br&gt;
Race conditions matter - Use atomic database operations for state changes&lt;br&gt;
Test security explicitly - Don't just test happy paths; test attack scenarios&lt;br&gt;
Distributed systems need distributed rate limiting - In-memory limits don't scale&lt;br&gt;
About Rugi Auth&lt;br&gt;
While working on these security improvements, I was building Rugi Auth—a centralized authentication service designed with security as a first-class concern. It includes:&lt;/p&gt;

&lt;p&gt;✅ RS256 JWT signing with RSA keys&lt;br&gt;
✅ Argon2id password hashing (memory-hard, timing-attack resistant)&lt;br&gt;
✅ Redis-backed distributed rate limiting with automatic fallback&lt;br&gt;
✅ Comprehensive security protections against timing attacks, IP spoofing, and race conditions&lt;br&gt;
✅ Extensive test coverage including security-focused tests&lt;br&gt;
✅ Multi-app role management for complex authorization needs&lt;br&gt;
If you're building authentication for your applications, I'd love for you to check it out. You can get started with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx rugi-auth init my-auth-server&lt;/code&gt;&lt;br&gt;
The project is open-source, and all the security improvements discussed in this article are implemented and tested. You can find it on GitHub and install it via npm.&lt;/p&gt;

&lt;p&gt;Resources&lt;br&gt;
&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authentication Cheat Sheet&lt;/a&gt;&lt;br&gt;
&lt;a href="https://en.wikipedia.org/wiki/Timing_attack" rel="noopener noreferrer"&gt;Timing Attack Prevention&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://expressjs.com/en/guide/behind-proxies.html" rel="noopener noreferrer"&gt;Express Trust Proxy Documentation&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://redis.io/glossary/rate-limiting" rel="noopener noreferrer"&gt;Redis Rate Limiting Best Practices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you encountered these vulnerabilities in your projects? What security measures do you implement in your authentication systems? Share your thoughts in the comments below!&lt;/p&gt;

</description>
      <category>node</category>
      <category>cybersecurity</category>
      <category>rugiauth</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
