<?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: Usman Murtaza</title>
    <description>The latest articles on DEV Community by Usman Murtaza (@usmanmurtaza).</description>
    <link>https://dev.to/usmanmurtaza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4141170%2F1a648c71-bd43-4f53-83ec-f67fae2f1c7b.jpg</url>
      <title>DEV Community: Usman Murtaza</title>
      <link>https://dev.to/usmanmurtaza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usmanmurtaza"/>
    <language>en</language>
    <item>
      <title>How I Built Maniesta Career OS: An AI-Powered Resume Builder and Job Matching Platform</title>
      <dc:creator>Usman Murtaza</dc:creator>
      <pubDate>Fri, 02 Oct 2026 23:43:54 +0000</pubDate>
      <link>https://dev.to/usmanmurtaza/how-i-built-maniesta-career-os-an-ai-powered-resume-builder-and-job-matching-platform-1f6h</link>
      <guid>https://dev.to/usmanmurtaza/how-i-built-maniesta-career-os-an-ai-powered-resume-builder-and-job-matching-platform-1f6h</guid>
      <description>&lt;h1&gt;
  
  
  Maniesta Career OS — Dev.to Article + LinkedIn + Twitter
&lt;/h1&gt;

&lt;p&gt;Yeh sab ready hai. Pehle &lt;strong&gt;Dev.to article&lt;/strong&gt; publish karo, phir &lt;strong&gt;LinkedIn&lt;/strong&gt; aur &lt;strong&gt;Twitter&lt;/strong&gt; pe share karo (URL ke saath).&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Dev.to Article — Complete Content
&lt;/h2&gt;

&lt;p&gt;Copy karo, apne naam se publish karo. Yeh placeholders nahi hain, full content hai.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# How I Built Maniesta Career OS: An AI-Powered Resume Builder and Job Matching Platform&lt;/span&gt;

Job hunting is broken. You spend hours crafting a resume, submit it to dozens of listings, and never hear back. Most of the time, an Applicant Tracking System (ATS) rejects your application before a human ever sees it.

I wanted to fix that. So I built &lt;span class="gs"&gt;**Maniesta Career OS**&lt;/span&gt; — an AI-powered career platform that generates ATS-optimised resumes, matches you with relevant jobs daily, and gives admins a full dashboard to manage it all.

🔗 &lt;span class="gs"&gt;**Live Demo:**&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;maniesta-career.netlify.app&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://maniesta-career.netlify.app/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

This post walks through how I designed the system, the technical decisions behind the AI integration, and the parts that were harder than expected.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## The Problem&lt;/span&gt;

Three problems, one platform:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Resumes fail ATS filters.**&lt;/span&gt; Most job seekers write resumes without knowing how ATS parsers read them. Keywords matter, structure matters, and formatting can make or break a submission.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Job matching is manual.**&lt;/span&gt; People scroll through dozens of job boards, hoping something sticks. There's no intelligent filtering — just endless lists.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Career tools are fragmented.**&lt;/span&gt; Resume builders, job boards, application trackers, and career dashboards all live in different apps. Users context-switch constantly.

Maniesta Career OS combines all three into one product.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Tech Stack Decisions&lt;/span&gt;

Here's what I picked and why:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**React**&lt;/span&gt; — component-driven UI, easy to iterate on the AI dashboard and admin views
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**TypeScript**&lt;/span&gt; — non-negotiable for a project with this many data shapes (users, resumes, jobs, applications, admin sessions)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Vite**&lt;/span&gt; — fast dev server, instant HMR, cleaner build output than CRA
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Firebase**&lt;/span&gt; — authentication, Firestore database, and cloud functions in one SDK; also easy to deploy
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Tailwind CSS**&lt;/span&gt; — for rapid UI on the admin dashboard and resume editor
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Gemini AI**&lt;/span&gt; — for resume content generation, ATS-optimisation suggestions, and job matching signals

The combination of Firebase + Gemini means I could ship an MVP quickly without running my own backend infrastructure.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Architecture Overview&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;┌──────────────────┐&lt;br&gt;
│   React Client   │&lt;br&gt;
│  (User + Admin)  │&lt;br&gt;
└────────┬─────────┘&lt;br&gt;
         │  HTTPS + Firebase Auth&lt;br&gt;
         ▼&lt;br&gt;
┌──────────────────┐&lt;br&gt;
│  Firebase        │&lt;br&gt;
│  - Auth          │&lt;br&gt;
│  - Firestore     │&lt;br&gt;
│  - Cloud Fn      │&lt;br&gt;
└────────┬─────────┘&lt;br&gt;
         │&lt;br&gt;
         ▼&lt;br&gt;
┌──────────────────┐&lt;br&gt;
│  Gemini AI API   │&lt;br&gt;
│  (Resume + Jobs) │&lt;br&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="nx"&gt;Two&lt;/span&gt; &lt;span class="nx"&gt;distinct&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="nx"&gt;journeys&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Job&lt;/span&gt; &lt;span class="nx"&gt;seekers&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="nx"&gt;resumes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="nx"&gt;applications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Admins&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;manage&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;monitor&lt;/span&gt; &lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="nx"&gt;Both&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="nx"&gt;Firebase&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;but&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;UI&lt;/span&gt; &lt;span class="nx"&gt;diverges&lt;/span&gt; &lt;span class="nx"&gt;sharply&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;which&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;why&lt;/span&gt; &lt;span class="nx"&gt;TypeScript&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="o"&gt;---&lt;/span&gt;

&lt;span class="err"&gt;##&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;Resume&lt;/span&gt; &lt;span class="nx"&gt;Generation&lt;/span&gt;

&lt;span class="nx"&gt;When&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="nx"&gt;creates&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;they&lt;/span&gt; &lt;span class="nx"&gt;provide&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;education&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;layer&lt;/span&gt; &lt;span class="nx"&gt;does&lt;/span&gt; &lt;span class="nx"&gt;three&lt;/span&gt; &lt;span class="nx"&gt;things&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="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Rewrites&lt;/span&gt; &lt;span class="nx"&gt;bullet&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;into&lt;/span&gt; &lt;span class="nx"&gt;achievement&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;ATS&lt;/span&gt; &lt;span class="nx"&gt;parsers&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt; &lt;span class="nx"&gt;well&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Suggests&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;based&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="nx"&gt;title&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="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;Scores&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;against&lt;/span&gt; &lt;span class="nx"&gt;ATS&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="nx"&gt;practices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="nx"&gt;engineering&lt;/span&gt; &lt;span class="nx"&gt;mattered&lt;/span&gt; &lt;span class="nx"&gt;more&lt;/span&gt; &lt;span class="nx"&gt;than&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="nx"&gt;itself&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Here&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s the shape of the function that generates suggestions:

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
typescript&lt;br&gt;
// services/ai.ts&lt;br&gt;
import { getGenerativeModel } from 'firebase/ai';&lt;/p&gt;

&lt;p&gt;const model = getGenerativeModel(ai, { model: 'gemini-pro' });&lt;/p&gt;

&lt;p&gt;export const generateResumeSuggestions = async (input: ResumeInput) =&amp;gt; {&lt;br&gt;
  const prompt = `&lt;br&gt;
    You are an ATS optimisation assistant. Rewrite the following bullet&lt;br&gt;
    points into achievement-focused language. Preserve all facts.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules:
- Start each bullet with a strong action verb
- Quantify impact where numbers are given
- Keep each bullet under 20 words
- Do not invent metrics or experiences

Bullet points:
${input.bullets.map((b, i) =&amp;gt; `${i + 1}. ${b}`).join('\n')}

Return a JSON array of the rewritten bullets in the same order.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;`;&lt;/p&gt;

&lt;p&gt;const result = await model.generateContent(prompt);&lt;br&gt;
  return JSON.parse(result.response.text());&lt;br&gt;
};&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
The key constraint I enforced: **the AI never invents facts.** It rewrites, reorganises, and suggests keywords — but it does not fabricate experiences or metrics. This was important for user trust.

---

## Daily Job Matching

Once a resume is saved, Maniesta Career OS runs a daily job match:

1. A scheduled Cloud Function runs every 24 hours.
2. For each user with an active resume, it fetches fresh job listings.
3. It computes a match score using the resume's keywords, the user's preferred location, and past applications.
4. It stores the top matches in Firestore.
5. The user sees them the next time they open the app.

The scoring is simple but effective — a weighted keyword overlap:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
typescript&lt;br&gt;
const computeMatchScore = (resume: Resume, job: Job): number =&amp;gt; {&lt;br&gt;
  const resumeKeywords = new Set(resume.keywords.map(k =&amp;gt; k.toLowerCase()));&lt;br&gt;
  const jobKeywords = job.tags.map(t =&amp;gt; t.toLowerCase());&lt;/p&gt;

&lt;p&gt;const overlap = jobKeywords.filter(k =&amp;gt; resumeKeywords.has(k)).length;&lt;br&gt;
  const keywordScore = overlap / Math.max(jobKeywords.length, 1);&lt;/p&gt;

&lt;p&gt;const locationScore = job.location === resume.preferredLocation ? 1 : 0.4;&lt;br&gt;
  const recencyScore = Math.max(0, 1 - job.ageInDays / 30);&lt;/p&gt;

&lt;p&gt;return keywordScore * 0.6 + locationScore * 0.25 + recencyScore * 0.15;&lt;br&gt;
};&lt;/p&gt;



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


Not fancy, but the weighting reflects real user behaviour — keywords matter most, then location, then freshness.

---

## The Admin Dashboard

Admins get a glassmorphism-styled dashboard with:

- **User management** — search, filter, deactivate accounts
- **Usage analytics** — daily AI calls, match rates, resume completion rates
- **Job pipeline** — how many jobs are being matched, which ones convert
- **System health** — Firebase usage, function invocations, error rates

The glassmorphism UI was a deliberate choice. It reads well on dark backgrounds and makes dense dashboards feel less clinical.

---

## Challenges I Didn't Expect

**1. TypeScript with Firestore generics.**
Firestore's type system is flexible but easy to loosen too much. I ended up defining strict schema types and a `withConverter` helper for every collection. Retrofitting this halfway through was painful.

**2. Prompt injection in resume content.**
Users can write anything in their resume bullets. A malicious user could try to make the AI produce unrelated content. I sanitise all user input before it hits the prompt — strip markdown, cap length, reject control characters.

**3. Firebase read costs.**
The daily job matching creates a lot of reads. I ended up using Firestore's batch writes and index tuning to keep reads under control. Real cost optimisation, not just code cleanliness.

---

## What I Learned

- **AI is a feature, not the product.** The value is in the workflow around the AI, not the AI itself.
- **TypeScript pays off at scale.** At 3 files you don't need it. At 60 files with 15 data shapes, it saves hours.
- **Prompt engineering is real engineering.** Bad prompts produce confidently wrong output.
- **Firebase is fast to start and slow to optimise.** Plan for cost from day one.
- **Glassmorphism looks good on dashboards** — it reduces visual fatigue in dense data views.

---

## Try It Out

- 🔗 **Live Demo:** [maniesta-career.netlify.app](https://maniesta-career.netlify.app/)
- 💻 **Source Code:** [github.com/usmannmurtazaa/Maniesta-careeros](https://github.com/usmannmurtazaa/Maniesta-careeros)

You can create a resume, see the AI suggestions, and view matched jobs. The admin side is behind authentication but the workflow is visible from the user side.

---

## What's Next

A few features in progress:

- **Application tracking** — a Kanban-style board for submitted applications
- **Interview prep** — AI-generated mock questions based on matched jobs
- **Resume versioning** — A/B test different resume versions against the same job set
- **Multi-language support** — resume generation in Urdu, Arabic, and Spanish

---

## About the Author

I'm **Usman Murtaza**, a Full Stack Developer based in Karachi, Pakistan. I build modern web applications with React, TypeScript, Node.js, and Firebase, and I'm the creator of the **Maniesta ecosystem** — a collection of web products and utilities.

**Maniesta Career OS** is one of those products. Others include Maniesta Campus, Maniesta Suite, Maniesta Veyra, and the Maniesta brand platform.

- 🌐 **Portfolio:** [usmanmurtaza.netlify.app](https://usmanmurtaza.netlify.app)
- 💻 **GitHub:** [github.com/Usmannmurtazaa](https://github.com/Usmannmurtazaa)
- 💼 **LinkedIn:** [linkedin.com/in/Usmannmurtazaa](https://www.linkedin.com/in/Usmannmurtazaa/)
- 🐦 **Twitter/X:** [@usman_murtazaa](https://twitter.com/usman_murtazaa)

If you found this useful, follow me here on Dev.to — I write about full-stack development, AI-assisted applications, and building products from scratch.

*Built by Usman Murtaza — see more projects at [usmanmurtaza.netlify.app](https://usmanmurtaza.netlify.app)*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Maniesta: Building a 12-Product Web Ecosystem Solo with React, Node.js, and TypeScript</title>
      <dc:creator>Usman Murtaza</dc:creator>
      <pubDate>Sat, 26 Sep 2026 01:50:09 +0000</pubDate>
      <link>https://dev.to/usmanmurtaza/maniesta-building-a-12-product-web-ecosystem-solo-with-react-nodejs-and-typescript-2n58</link>
      <guid>https://dev.to/usmanmurtaza/maniesta-building-a-12-product-web-ecosystem-solo-with-react-nodejs-and-typescript-2n58</guid>
      <description>&lt;p&gt;Every developer I know has a folder on their laptop called "ideas" that never gets built.&lt;/p&gt;

&lt;p&gt;Two years ago, mine had about 40 entries. Today, 12 of them are live products that real people use, all sitting under one brand: &lt;strong&gt;Maniesta&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a "how I got rich" post. It's a breakdown of what it actually looks like to ship a whole product ecosystem alone — the technical decisions, the trade-offs, and the things nobody warns you about when you decide to build 12 things instead of polishing 1.&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Ecosystem:&lt;/strong&gt; &lt;a href="https://maniesta.netlify.app/" rel="noopener noreferrer"&gt;maniesta.netlify.app&lt;/a&gt;&lt;br&gt;
👤 &lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://usmanmurtaza.netlify.app" rel="noopener noreferrer"&gt;usmanmurtaza.netlify.app&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is Maniesta?
&lt;/h2&gt;

&lt;p&gt;Maniesta is a web product ecosystem — a collection of tools, apps, and platforms that share a design language, a code philosophy, and a single creator (me).&lt;/p&gt;

&lt;p&gt;The products fall into three loose categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Education &amp;amp; Productivity&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Campus&lt;/strong&gt; — role-based campus management system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta School ERP&lt;/strong&gt; — multi-portal school dashboard with attendance, fees, notices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Notes&lt;/strong&gt; — real-time notes app with auth, tags, and archive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Suite&lt;/strong&gt; — GPA, CGPA, and scientific calculator platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Creative &amp;amp; Consumer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Play&lt;/strong&gt; — music discovery and listening app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Weather&lt;/strong&gt; — real-time weather dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Label&lt;/strong&gt; — fashion e-commerce storefront&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Veyra&lt;/strong&gt; — clothing e-commerce + custom print studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Business &amp;amp; AI&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta ResumeAI&lt;/strong&gt; — AI-powered resume builder with ATS optimisation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ResumeAI Pro&lt;/strong&gt; — standalone resume builder SaaS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta AI Travel Planner&lt;/strong&gt; — Gemini-powered itinerary generator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta Digital&lt;/strong&gt; — the agency site that ties it all together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is live. Each one is used by someone. None of them are perfect.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architecture Decision That Changed Everything
&lt;/h2&gt;

&lt;p&gt;The first two products I built had completely separate codebases. Different auth systems. Different design tokens. Different deployment pipelines. Different bugs.&lt;/p&gt;

&lt;p&gt;By product three, I was maintaining three versions of the same login component.&lt;/p&gt;

&lt;p&gt;So I made a decision that I still debate to this day: &lt;strong&gt;do I unify the ecosystem, or keep the products independent?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I chose a middle path — &lt;strong&gt;shared conventions, separate deployments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One design language&lt;/strong&gt; — same colour tokens, same spacing scale, same glassmorphism treatment. Not enforced through a shared library, just through discipline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same tech stack where it makes sense&lt;/strong&gt; — React for interactive apps, Vanilla JS for lightweight tools, Next.js when SEO mattered more than app-feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deployments&lt;/strong&gt; — every product is its own Netlify site. No monorepo, no shared runtime, no cascading failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-linking over shared state&lt;/strong&gt; — Maniesta products link to each other through footers and headers, but they don't share sessions or data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This worked. If Maniesta Play goes down, Maniesta Campus keeps running. If I want to rewrite Maniesta Notes in a different framework, no other product is affected.&lt;/p&gt;

&lt;p&gt;But there's a cost: &lt;strong&gt;every project repeats some boilerplate&lt;/strong&gt;. Auth setup, deploy config, design tokens. It's the tax you pay for independence.&lt;/p&gt;

&lt;p&gt;For a solo developer, I think it's the right tax.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Stack — What I Actually Use
&lt;/h2&gt;

&lt;p&gt;Across the ecosystem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React for interactive apps (Campus, ResumeAI, School ERP)&lt;/li&gt;
&lt;li&gt;Next.js when I needed SSR and SEO (Veyra, AI Travel Planner)&lt;/li&gt;
&lt;li&gt;Vanilla JS for lightweight utilities (Notes, Weather, Play)&lt;/li&gt;
&lt;li&gt;TypeScript on newer projects, JavaScript on the earlier ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailwind CSS for most new work&lt;/li&gt;
&lt;li&gt;styled-components on the older React apps&lt;/li&gt;
&lt;li&gt;Plain CSS on the utility tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js + Express for custom APIs&lt;/li&gt;
&lt;li&gt;Firebase for auth, database, and real-time features&lt;/li&gt;
&lt;li&gt;PostgreSQL + Prisma when the data model was relational&lt;/li&gt;
&lt;li&gt;MongoDB when the schema was evolving fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI API for ResumeAI Pro&lt;/li&gt;
&lt;li&gt;Gemini AI for the Travel Planner&lt;/li&gt;
&lt;li&gt;Both integrated through simple server-side proxies, never exposed to the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Netlify for everything. All 12 products run on the free tier.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What "Solo" Actually Means
&lt;/h2&gt;

&lt;p&gt;When people hear "solo developer," they imagine one person doing the work of five.&lt;/p&gt;

&lt;p&gt;The reality is different. Solo means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are the product manager. Decisions that would normally be argued out in meetings are made by you, alone, at 2 AM, with incomplete information.&lt;/li&gt;
&lt;li&gt;You are the designer. Every spacing value, every colour, every animation curve — yours.&lt;/li&gt;
&lt;li&gt;You are the QA team. Bugs ship more often than they should.&lt;/li&gt;
&lt;li&gt;You are the DevOps engineer. Deploy fails at 11 PM? That's your Friday night.&lt;/li&gt;
&lt;li&gt;You are customer support. Users email you directly. You reply from your phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The upside is total control. The downside is that everything you don't do &lt;strong&gt;doesn't get done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Some Maniesta products have a README. Some don't. Some have tests. Most don't. There is no engineering manager to say "you should write docs for this." There's only me, and I'd rather ship the next feature than write the docs for the last one.&lt;/p&gt;

&lt;p&gt;This is a real trade-off. I'm not proud of it. But it's honest.&lt;/p&gt;


&lt;h2&gt;
  
  
  The RBAC Problem — Solved Once, Reused Everywhere
&lt;/h2&gt;

&lt;p&gt;Multiple Maniesta products needed role-based access control. Campus has admin/faculty/student. School ERP has admin/teacher/parent. ResumeAI has user/admin.&lt;/p&gt;

&lt;p&gt;I learned early that &lt;strong&gt;RBAC bolted on later is RBAC that leaks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the first time I built it, I built it properly. A single pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Verify the JWT, attach the user to the request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt; &lt;span class="o"&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="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="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;token&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;401&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="s1"&gt;No token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;try&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&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="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;JWT_SECRET&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="k"&gt;catch&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&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="s1"&gt;Invalid token&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;// 2. Restrict the route to specific roles&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireRole&lt;/span&gt; &lt;span class="o"&gt;=&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="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="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;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;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;role&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="s1"&gt;Forbidden&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="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;// 3. Compose them in routes&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/students/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;authenticate&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="nx"&gt;deleteStudent&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Maniesta product that needed RBAC copied this pattern. Not the code — just the pattern. It's small enough that repeating it is fine, and it's isolated enough that changing it in one place doesn't break others.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;patterns scale better than packages&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Handle the Ecosystem Branding
&lt;/h2&gt;

&lt;p&gt;Every Maniesta product needs to feel like part of the same family, without feeling like the same app.&lt;/p&gt;

&lt;p&gt;The formula I settled on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same visual DNA&lt;/strong&gt; — dark background (&lt;code&gt;#0c0c1d&lt;/code&gt;), aurora gradient accents, glassmorphism surfaces, one primary purple accent, one cyan accent for emphasis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different personality&lt;/strong&gt; — Campus is dense and functional. Play is playful and spacious. Notes is minimal. Weather is dashboard-like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same footer pattern&lt;/strong&gt; — every product's footer says:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Maniesta Campus&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; — a project by
  &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;"https://usmanmurtaza.netlify.app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Usman Murtaza&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
  Part of the
  &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;"https://maniesta.netlify.app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Maniesta ecosystem&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This footer is doing two jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For users:&lt;/strong&gt; letting them discover the other products&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Google:&lt;/strong&gt; connecting every product to one creator and one brand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mistakes I Made (and Still Make)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Building too many things at once.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
For a while I was working on Campus, ResumeAI, and Play simultaneously. None of them got the attention they needed. When I focused on one product at a time and shipped it fully, the quality jumped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Skipping documentation.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Six months later I can't remember why I structured a specific module the way I did. A 3-line comment would have saved hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Not versioning the design system early.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
I changed the accent colour halfway through the ecosystem. Rebuilding the older products to match took an entire weekend. If I'd written down the design tokens from day one, that weekend would have been one hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Assuming people would find the products.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Shipping is 10% of the work. Distribution is the other 90%. Dev.to articles, LinkedIn posts, GitHub descriptions — none of that happens automatically. I had to build the habit.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell Someone Starting Today
&lt;/h2&gt;

&lt;p&gt;If you're thinking about building your own product ecosystem — even a small one — here's what I've learned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship the first product before you design the brand.&lt;/strong&gt; Brand comes from shipping, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solve one pattern really well, then reuse it.&lt;/strong&gt; Auth, RBAC, deployment config, error handling. These repeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep products independent.&lt;/strong&gt; A monorepo saves you 5 minutes of typing and costs you a weekend of debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write in public.&lt;/strong&gt; Every product I've written about has found users. Every silent product hasn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the tech to the product, not the other way around.&lt;/strong&gt; React for interactivity, Next.js for SEO, Vanilla JS for simplicity. Don't force one tool everywhere.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maniesta One&lt;/strong&gt; — a unified entry point that lets users jump between products from one place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-product auth&lt;/strong&gt; — optional single sign-on for users who want it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-sourcing a few of the utility tools&lt;/strong&gt; — Notes, Weather, and Play are candidates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More writing&lt;/strong&gt; — this is the first of several articles about building the ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something similar — a family of small products instead of one big one — I'd love to hear about it. Drop a comment or reach out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try Maniesta
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Ecosystem hub:&lt;/strong&gt; &lt;a href="https://maniesta.netlify.app" rel="noopener noreferrer"&gt;maniesta.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎓 &lt;strong&gt;Maniesta Campus:&lt;/strong&gt; &lt;a href="https://maniestacampus.netlify.app" rel="noopener noreferrer"&gt;maniestacampus.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Maniesta Notes:&lt;/strong&gt; &lt;a href="https://maniestanotes.netlify.app" rel="noopener noreferrer"&gt;maniestanotes.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌤️ &lt;strong&gt;Maniesta Weather:&lt;/strong&gt; &lt;a href="https://maniestaweather.netlify.app" rel="noopener noreferrer"&gt;maniestaweather.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎵 &lt;strong&gt;Maniesta Play:&lt;/strong&gt; &lt;a href="https://maniestaplay.netlify.app" rel="noopener noreferrer"&gt;maniestaplay.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Usman Murtaza&lt;/strong&gt;, a Full Stack Developer based in Karachi, Pakistan. I build modern web applications with React, Node.js, TypeScript, and Firebase — and I'm the creator of the &lt;strong&gt;Maniesta ecosystem&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://usmanmurtaza.netlify.app" rel="noopener noreferrer"&gt;usmanmurtaza.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Usmannmurtazaa" rel="noopener noreferrer"&gt;github.com/Usmannmurtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/Usmannmurtazaa/" rel="noopener noreferrer"&gt;linkedin.com/in/Usmannmurtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Twitter/X:&lt;/strong&gt; &lt;a href="https://twitter.com/usman_murtazaa" rel="noopener noreferrer"&gt;@usman_murtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed this, follow me here on Dev.to — I'll be writing more about the Maniesta ecosystem, the technical decisions behind each product, and the realities of building alone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Usman Murtaza — see the full ecosystem at &lt;a href="https://maniesta.netlify.app" rel="noopener noreferrer"&gt;maniesta.netlify.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>node</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built Maniesta Campus OS: A Multi-Tenant Student Management SaaS with React and Firebase</title>
      <dc:creator>Usman Murtaza</dc:creator>
      <pubDate>Sat, 26 Sep 2026 00:40:14 +0000</pubDate>
      <link>https://dev.to/usmanmurtaza/how-i-built-maniesta-campus-os-a-multi-tenant-student-management-saas-with-react-and-firebase-3akc</link>
      <guid>https://dev.to/usmanmurtaza/how-i-built-maniesta-campus-os-a-multi-tenant-student-management-saas-with-react-and-firebase-3akc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgek5t6wdifl9drv26ma6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgek5t6wdifl9drv26ma6.png" alt=" " width="799" height="370"&gt;&lt;/a&gt;Educational institutions run on spreadsheets. Student records in one file, attendance in another, grades somewhere else, and course schedules — usually — in someone's head.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Maniesta Campus OS&lt;/strong&gt; to fix that. It is a multi-tenant student management platform where an institute can manage students, courses, attendance, and marks through a single dashboard with role-based access.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://maniestacampus.netlify.app/" rel="noopener noreferrer"&gt;maniestacampus.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post walks through the architecture, the multi-tenancy model, the RBAC system, and the parts that were harder than expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Before writing any code, I mapped what an actual institute needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admins&lt;/strong&gt; manage students, courses, faculty, and enrollments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teachers&lt;/strong&gt; mark attendance, upload grades, and see their class rosters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Students&lt;/strong&gt; check schedules, grades, and enrolled courses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple institutes&lt;/strong&gt; should be able to use the same platform without seeing each other's data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last requirement is what makes this a SaaS problem rather than just a CRUD app. Real institutional software is multi-tenant by default — and multi-tenancy is where most of the design effort goes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack Decisions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 18, React Router 6&lt;/td&gt;
&lt;td&gt;Component-based UI with route-level separation for admin, teacher, student views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;Fast iteration across three different dashboard layouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Firebase (Auth, Firestore, Storage)&lt;/td&gt;
&lt;td&gt;No servers to manage, real-time by default, scales to zero cost on free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Firebase Authentication&lt;/td&gt;
&lt;td&gt;Email/Password + Google sign-in out of the box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Cloud Firestore&lt;/td&gt;
&lt;td&gt;Document store fits hierarchical campus data; flexible schema per role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;Recharts&lt;/td&gt;
&lt;td&gt;Lightweight, composable, works well with Firestore data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animations&lt;/td&gt;
&lt;td&gt;Framer Motion&lt;/td&gt;
&lt;td&gt;Spring-based transitions that feel native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Netlify&lt;/td&gt;
&lt;td&gt;Zero-config CI/CD from Git, edge CDN built in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No backend server. The client talks directly to Firebase, guarded by Firestore Security Rules. For a project at this scale that is a feature, not a limitation — it keeps the operational surface area near zero.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────┐
│     React SPA (Netlify)    │
│  - Auth pages              │
│  - Org-scoped portal       │
│  - Admin console           │
└───────────┬────────────────┘
            │ HTTPS + Firebase SDK
┌───────────▼────────────────┐
│       Firebase Cloud       │
│  - Authentication          │
│  - Firestore Database      │
│  - Storage (optional)      │
│  - Security Rules          │
└────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Firestore document carries an &lt;code&gt;orgId&lt;/code&gt; field. Every query filters by &lt;code&gt;orgId&lt;/code&gt;. Every security rule checks &lt;code&gt;orgId&lt;/code&gt; against the authenticated user's profile. This is the entire multi-tenancy model in one sentence.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Part: Multi-Tenancy + RBAC
&lt;/h2&gt;

&lt;p&gt;Two things had to work together from day one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy&lt;/strong&gt; — an institute called "Springfield Academy" must never see data belonging to "Riverside College".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based access&lt;/strong&gt; — a student should not see admin controls; a teacher should not be able to edit another teacher's course.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I made three decisions upfront that kept the rest of the project sane.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 1 — Single &lt;code&gt;users&lt;/code&gt; collection with a role field
&lt;/h3&gt;

&lt;p&gt;Rather than separate collections for admins, teachers, and students, I used one collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// users/{uid}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ali@springfield.edu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ali Khan&lt;/span&gt;&lt;span class="dl"&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="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="c1"&gt;// 'admin' | 'teacher' | 'student'&lt;/span&gt;
  &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;springfield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// which institute this user belongs to&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;timestamp&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One auth flow. One user profile fetch. If a student is later promoted to a teacher, I update one field — no data migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 2 — &lt;code&gt;orgId&lt;/code&gt; on every document
&lt;/h3&gt;

&lt;p&gt;Every course, student, mark, and attendance record has an &lt;code&gt;orgId&lt;/code&gt;. This makes isolation explicit and enforceable at the database level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// courses/{courseId}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;springfield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CS-400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Software Engineering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instructor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Prof. Fatima Rizvi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;totalStudents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;timestamp&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Decision 3 — Enforce isolation in Security Rules, not just in code
&lt;/h3&gt;

&lt;p&gt;Client-side filters are a convenience. The real guard is the security rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function userOrgId() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.orgId;
    }

    match /courses/{courseId} {
      allow read: if request.auth != null
                  &amp;amp;&amp;amp; userOrgId() == resource.data.orgId;
      allow write: if request.auth != null
                   &amp;amp;&amp;amp; userOrgId() == request.data.orgId;
    }

    match /students/{studentId} {
      allow read: if request.auth != null
                  &amp;amp;&amp;amp; userOrgId() == resource.data.orgId;
      allow write: if request.auth != null
                   &amp;amp;&amp;amp; userOrgId() == request.data.orgId;
    }

    // ... same pattern for marks, attendance
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if a client-side bug accidentally queries the wrong &lt;code&gt;orgId&lt;/code&gt;, Firestore refuses to return or write data outside the caller's organization. Multi-tenancy becomes a database guarantee, not just a frontend convention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frontend: One App, Three Dashboards
&lt;/h2&gt;

&lt;p&gt;The React shell renders three completely different dashboards based on the user's role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&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="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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;switch &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;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdminDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;teacher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TeacherDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;student&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StudentDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&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="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;Each dashboard imports its own components. The shared shell — navbar, sidebar, header, footer — lives outside and is reused across all three.&lt;/p&gt;

&lt;p&gt;A small &lt;code&gt;ProtectedRoute&lt;/code&gt; wrapper handles the auth check on the way in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProtectedRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&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;user&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&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="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&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;Adding a new admin-only page is now a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProtectedRoute&lt;/span&gt; &lt;span class="na"&gt;allowedRoles&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="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="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="nc"&gt;ManageFaculty&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="nc"&gt;ProtectedRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Real-Time Attendance
&lt;/h2&gt;

&lt;p&gt;Attendance is where Firestore shines. Instead of a traditional "save then refresh" flow, attendance is written as individual documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// attendance/{attendanceId}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;springfield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;courseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs-400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;courseName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Software Engineering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ali-khan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;studentName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ali Khan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-26&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;present&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// 'present' | 'absent'&lt;/span&gt;
  &lt;span class="nx"&gt;recordedBy&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-uid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="nx"&gt;timestamp&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The teacher's view subscribes to today's records filtered by &lt;code&gt;courseId&lt;/code&gt;. When a teacher marks someone present, the chart on the admin dashboard updates instantly — no polling, no refresh button, no manual consolidation step.&lt;/p&gt;

&lt;p&gt;The weekly attendance trend on the dashboard is derived by querying the last seven days and counting present vs. absent. It sounds trivial, but the fact that data is already normalized makes the aggregation a 20-line function instead of a data pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Smart Marksheets
&lt;/h2&gt;

&lt;p&gt;Marks follow the same pattern. Each mark is a document with the obtained score, the total, the computed percentage, and the letter grade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// marks/{markId}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;springfield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ali-khan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;studentName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ali Khan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;courseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs-400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;courseName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Software Engineering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;examType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mid-term&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;obtainedMarks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;totalMarks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recordedBy&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-uid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="nx"&gt;timestamp&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grades are computed at write time with a simple scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gradeFromMarks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marks&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="nx"&gt;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A+&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;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&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;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;B&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;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;C&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;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;D&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;marks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;F&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;p&gt;I deliberately kept grading logic on the client for now. If institutes ever need custom scales, the field would move to a per-organization settings document and the function would read it at write time — but I did not want to solve that problem before a customer actually asked for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Firestore Indexes — the Part Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;This one cost me an afternoon. Firestore requires a &lt;strong&gt;composite index&lt;/strong&gt; whenever you combine a &lt;code&gt;where&lt;/code&gt; filter with an &lt;code&gt;orderBy&lt;/code&gt; on different fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orgId&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="s1"&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;orgId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;courseId&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="s1"&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;courseId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&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="s1"&gt;desc&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;p&gt;Firestore will refuse to run this query until a &lt;code&gt;(orgId, courseId, createdAt)&lt;/code&gt; index exists. The error message includes a direct link to create it, but on a fresh project this throws immediately in development and looks like a bug in your code.&lt;/p&gt;

&lt;p&gt;The fix is to declare indexes in a version-controlled file so they deploy with the rest of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;firestore.indexes.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"indexes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"collectionGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"queryScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COLLECTION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orgId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASCENDING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"courseId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASCENDING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DESCENDING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"collectionGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attendance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"queryScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COLLECTION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orgId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASCENDING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASCENDING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx firebase deploy &lt;span class="nt"&gt;--only&lt;/span&gt; firestore:indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every environment has the same indexes. No more "works on my machine".&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design multi-tenancy before writing features.&lt;/strong&gt; Retrofitting &lt;code&gt;orgId&lt;/code&gt; into a live app is brutal. Getting it right upfront made everything else easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce isolation at the database, not the client.&lt;/strong&gt; Security rules are not optional — they are the whole point of building a multi-tenant product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firestore composite indexes are a real constraint.&lt;/strong&gt; They should live in version control alongside the schema, not be created ad-hoc from error messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component composition in React scales beautifully for dashboards.&lt;/strong&gt; Three completely different UIs coexisting in one app turned out to be cleaner than I expected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No backend is a real architecture, not a shortcut.&lt;/strong&gt; Removing the server did not remove the design work — it just moved it to security rules, client-side access patterns, and index declarations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://maniestacampus.netlify.app/" rel="noopener noreferrer"&gt;maniestacampus.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/usmannmurtazaa/maniesta-campus-os" rel="noopener noreferrer"&gt;github.com/usmannmurtazaa/maniesta-campus-os&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The RBAC behavior is the most interesting part to explore — sign in with different roles and watch the navigation and available actions change.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Some features I am working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email notifications for password reset and announcements&lt;/li&gt;
&lt;li&gt;Invite-based organization joining&lt;/li&gt;
&lt;li&gt;Student and parent portal with grade history&lt;/li&gt;
&lt;li&gt;File uploads for marksheets and documents&lt;/li&gt;
&lt;li&gt;Subscription plans and billing integration&lt;/li&gt;
&lt;li&gt;Enhanced analytics with exportable reports&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;I am &lt;strong&gt;Usman Murtaza&lt;/strong&gt;, a Full Stack Developer based in Karachi, Pakistan. I build modern web applications with React, Node.js, and Firebase, and I am the creator of the &lt;strong&gt;Maniesta ecosystem&lt;/strong&gt; — a collection of web products and utilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maniesta Campus OS&lt;/strong&gt; is one of those products. Others include Maniesta Resume AI, Maniesta Suite, and the Maniesta brand platform itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://usmanmurtaza.netlify.app" rel="noopener noreferrer"&gt;usmanmurtaza.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Usmannmurtazaa" rel="noopener noreferrer"&gt;github.com/Usmannmurtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/Usmannmurtazaa/" rel="noopener noreferrer"&gt;linkedin.com/in/Usmannmurtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Twitter/X:&lt;/strong&gt; &lt;a href="https://twitter.com/usman_murtazaa" rel="noopener noreferrer"&gt;@usman_murtazaa&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/usmanmurtaza"&gt;dev.to/usmanmurtaza&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this useful, follow me here on Dev.to — I write about full-stack development, React patterns, and building products from scratch.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by Usman Murtaza — see more projects at &lt;a href="https://usmanmurtaza.netlify.app" rel="noopener noreferrer"&gt;usmanmurtaza.netlify.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;



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


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>react</category>
      <category>firebase</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How I Built Maniesta Campus: A Campus Management System with React and Node.js</title>
      <dc:creator>Usman Murtaza</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:50:51 +0000</pubDate>
      <link>https://dev.to/usmanmurtaza/how-i-built-maniesta-campus-a-campus-management-system-with-react-and-nodejs-4hc8</link>
      <guid>https://dev.to/usmanmurtaza/how-i-built-maniesta-campus-a-campus-management-system-with-react-and-nodejs-4hc8</guid>
      <description>&lt;p&gt;Educational institutions run on paperwork and spreadsheets. Student records live in one file, attendance in another, grades somewhere else, and schedules — usually — in someone's head.&lt;/p&gt;

&lt;p&gt;I kept hearing the same complaint from small colleges and coaching centers: "We need something simple that just works." So I built &lt;strong&gt;Maniesta Campus&lt;/strong&gt; — a role-based campus management system with separate dashboards for admins, faculty, and students.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://maniestacampus.netlify.app/" rel="noopener noreferrer"&gt;maniestacampus.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post walks through how I designed the system, the technical decisions I made, and the parts that were harder than expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, I mapped out what an actual campus needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Admins&lt;/strong&gt; need to manage students, faculty, courses, and enrollments from one place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faculty&lt;/strong&gt; need to mark attendance, upload grades, and view their class rosters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Students&lt;/strong&gt; need to see their schedule, grades, and enrolled courses without asking anyone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core challenge was &lt;strong&gt;role-based access&lt;/strong&gt; — one login system that serves three completely different user journeys. A student should never see admin controls, and a faculty member shouldn't be able to edit another faculty member's course.&lt;/p&gt;

&lt;p&gt;Traditional CRUD apps don't handle this well. You end up with &lt;code&gt;if (user.role === 'admin')&lt;/code&gt; scattered everywhere. I wanted something cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack Decisions
&lt;/h2&gt;

&lt;p&gt;Here's what I picked and why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; — the UI needed to render completely different dashboards based on role. Component composition made this natural.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js + Express&lt;/strong&gt; — a straightforward REST API that I could iterate on quickly. No need for anything heavier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB&lt;/strong&gt; — campus data is hierarchical and evolves fast. Student records have different fields than faculty records, and course structures change per institution. Flexible schemas saved me from constant migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt; — stateless authentication means the API doesn't need session storage. Scales horizontally, and the frontend can hold the token with the user's role embedded in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — for fast UI iteration. When you're building three dashboards, you don't want to write custom CSS for every component.&lt;/li&gt;
&lt;/ul&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
┌──────────────────┐
│   React Client   │
│  (3 dashboards)  │
└────────┬─────────┘
│  HTTP + JWT
▼
┌──────────────────┐
│  Express API     │
│  - Auth routes   │
│  - RBAC middleware│
│  - Resource routes│
└────────┬─────────┘
│
▼
┌──────────────────┐
│    MongoDB       │
│  users, courses, │
│  grades, etc.    │
└──────────────────┘

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request flows through an authentication middleware first, then a role-check middleware, then the actual route handler. Nothing reaches the database without passing the role gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core: Role-Based Access Control
&lt;/h2&gt;

&lt;p&gt;The RBAC system is where most of the design effort went. Here's the middleware that powers it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware/auth.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsonwebtoken&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 the JWT and attach the user to the request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt; &lt;span class="o"&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="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="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;token&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;401&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="s1"&gt;No token provided&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&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="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;JWT_SECRET&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { id, role }&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;401&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="s1"&gt;Invalid token&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;// Only allow specific roles to proceed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireRole&lt;/span&gt; &lt;span class="o"&gt;=&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="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="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;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;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;role&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="s1"&gt;Access denied&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="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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authenticate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any route can declare its own access rules cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/courses.js&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;authenticate&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../middleware/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;// Anyone logged in can view courses&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;/courses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listCourses&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Only admins can create courses&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;/courses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;authenticate&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="nx"&gt;createCourse&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Faculty can update grades for their own courses&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;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/courses/:id/grades&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;authenticate&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;faculty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;updateGrades&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern scales. If I ever add a "teaching assistant" role, I just pass it into requireRole — no other code changes.&lt;/p&gt;




&lt;p&gt;Frontend: One App, Three Dashboards&lt;/p&gt;

&lt;p&gt;On the frontend, I used a simple pattern — the auth context holds the user, and the dashboard component switches on the role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&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="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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;switch &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;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdminDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;faculty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FacultyDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;student&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StudentDashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&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="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;Each dashboard imports its own set of components. The shared pieces — navbar, notifications, profile, logout — live outside and are reused across all three.&lt;/p&gt;

&lt;p&gt;A protected route wrapper handles the auth check on the way in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProtectedRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowedRoles&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;user&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Navigate&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&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="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&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;With this in place, adding a new admin-only page is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProtectedRoute&lt;/span&gt; &lt;span class="na"&gt;allowedRoles&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="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="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="nc"&gt;ManageFaculty&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="nc"&gt;ProtectedRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Challenges I Didn't Expect&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Schema design for multiple user types.&lt;br&gt;
At first I considered separate collections for students, faculty, and admins. That meant three login flows and three auth systems. Then I realized a single users collection with a role field plus role-specific optional fields is much simpler. When a student is also a teaching assistant, I just update the role — no data duplication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keeping the JWT payload small.&lt;br&gt;
I only embed { id, role } in the token. Everything else — name, email, permissions — is fetched on demand. Bigger tokens are slower to verify, and they become stale as soon as a user updates their profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grade calculation edge cases.&lt;br&gt;
What counts as an "A"? Every institution has its own grading scale. I ended up making the scale configurable per institution and storing it in the database, so the grading logic stays data-driven rather than hardcoded.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What I Learned&lt;/p&gt;

&lt;p&gt;· Design RBAC before writing features. Retrofitting access control into an existing app is brutal. Getting it right upfront made everything else easier.&lt;br&gt;
· Keep JWTs thin. Only store what the token actually needs to prove identity. Fetch the rest.&lt;br&gt;
· Data-driven grading beats code-driven grading. When in doubt, make it configurable.&lt;br&gt;
· Component composition in React scales beautifully for dashboards. Three completely different UIs coexisting in one app turned out to be much cleaner than I expected.&lt;/p&gt;




&lt;p&gt;Try It Out&lt;/p&gt;

&lt;p&gt;· 🔗 Live Demo: maniestacampus.netlify.app&lt;br&gt;
· 💻 Source Code: github.com/usmannmurtazaa/maniesta-campus-os&lt;/p&gt;

&lt;p&gt;You can log in as different roles to see how the dashboards change. The RBAC behaviour is the most interesting part to explore.&lt;/p&gt;




&lt;p&gt;What's Next&lt;/p&gt;

&lt;p&gt;A few features I'm planning:&lt;/p&gt;

&lt;p&gt;· Real-time notifications with Socket.io for grade updates and enrollment changes&lt;br&gt;
· An analytics dashboard for admins (enrollment trends, popular courses)&lt;br&gt;
· A mobile app built with React Native that shares the same API&lt;br&gt;
· Bulk import from Excel so institutions can migrate their existing records&lt;/p&gt;




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

&lt;p&gt;I'm Usman Murtaza, a Full Stack Developer based in Karachi, Pakistan. I build modern web applications with React, Node.js, and MongoDB, and I'm the creator of the Maniesta ecosystem — a collection of web products and utilities.&lt;/p&gt;

&lt;p&gt;Maniesta Campus is one of those products. Others include Maniesta Resume AI, Maniesta Suite, and the Maniesta brand platform itself.&lt;/p&gt;

&lt;p&gt;· 🌐 Portfolio: usmanmurtaza.netlify.app&lt;br&gt;
· 💻 GitHub: github.com/Usmannmurtazaa&lt;br&gt;
· 💼 LinkedIn: linkedin.com/in/Usmannmurtazaa&lt;br&gt;
· 🐦 Twitter/X: @usman_murtazaa&lt;/p&gt;

&lt;p&gt;If you found this useful, follow me here on Dev.to — I write about full-stack development, React patterns, and building products from scratch.&lt;/p&gt;

&lt;p&gt;Built by Usman Murtaza - see more projects at usmanmurtaza.netlify.app&lt;/p&gt;

</description>
      <category>fullstack</category>
      <category>node</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
