<?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: VATSRAJ PARMAR</title>
    <description>The latest articles on DEV Community by VATSRAJ PARMAR (@vatsraj_parmar_17a32201fd).</description>
    <link>https://dev.to/vatsraj_parmar_17a32201fd</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%2F4086462%2Ff196ebd4-81aa-400a-9840-e8fc53a87971.jpeg</url>
      <title>DEV Community: VATSRAJ PARMAR</title>
      <link>https://dev.to/vatsraj_parmar_17a32201fd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vatsraj_parmar_17a32201fd"/>
    <language>en</language>
    <item>
      <title>The Daily Habit That Actually Fixed My Team Visibility Problem</title>
      <dc:creator>VATSRAJ PARMAR</dc:creator>
      <pubDate>Mon, 24 Aug 2026 06:38:22 +0000</pubDate>
      <link>https://dev.to/vatsraj_parmar_17a32201fd/the-daily-habit-that-actually-fixed-my-team-visibility-problem-1l0k</link>
      <guid>https://dev.to/vatsraj_parmar_17a32201fd/the-daily-habit-that-actually-fixed-my-team-visibility-problem-1l0k</guid>
      <description>&lt;p&gt;A few months ago I moved from a pure dev role into coordinating a team of 8 people — still writing code myself, but now also responsible for knowing what everyone else is working on, day to day.&lt;/p&gt;

&lt;p&gt;The problem showed up almost immediately: I had no idea what my team was actually doing on any given day unless I asked them directly. Slack messages got buried. "Quick status check-ins" turned into 20-minute conversations that derailed everyone's focus. By the time something was actually blocked, I'd usually find out a day or two late — right when a client asked why it wasn't done yet.&lt;/p&gt;

&lt;p&gt;The fix wasn't a tool, it was a habit&lt;/p&gt;

&lt;p&gt;I didn't reach for new project management software. I built one habit: a daily consolidated report, same format every single day, no exceptions.&lt;/p&gt;

&lt;p&gt;It's simple on paper — one sheet, one row per team member, color-coded by status (on track / at risk / blocked), a short one-line note on what they're working on, and a red banner at the top if anyone hasn't submitted their update yet.&lt;/p&gt;

&lt;p&gt;That's it. No new app to learn, no login for anyone else, just a five-minute daily input from each person and a five-minute review from me.&lt;/p&gt;

&lt;p&gt;Why this worked when ad-hoc check-ins didn't&lt;/p&gt;

&lt;p&gt;It made silence visible. If someone didn't submit, the red banner at the top made that immediately obvious — not buried in a scroll of messages, not something I had to remember to chase. The absence of an update became as informative as the update itself.&lt;/p&gt;

&lt;p&gt;It's asynchronous by default. Nobody has to be in a meeting for this to work. People fill it in when they naturally hit a stopping point, not on my schedule.&lt;/p&gt;

&lt;p&gt;It's boring, which is the point. The format never changes. Nobody has to think about how to report, just what happened. Decision fatigue kills habits like this fast — remove every unnecessary choice and it actually sticks.&lt;/p&gt;

&lt;p&gt;It surfaces patterns you can't see day-to-day. A week of these reports side by side shows you things a single day never will — who's consistently blocked on the same dependency, whose "at risk" tasks never actually resolve, where the real bottleneck in your process is.&lt;/p&gt;

&lt;p&gt;What I'd tell someone starting this from scratch&lt;/p&gt;

&lt;p&gt;Keep the format rigid at first, even if it feels too simple. The temptation is to add more columns, more nuance, more categories — resist it for at least a few weeks. A habit that's slightly too simple still gets used. A habit that's perfectly comprehensive but takes 15 minutes to fill in gets skipped the first busy day, and once it's skipped once, it's much easier to skip again.&lt;/p&gt;

&lt;p&gt;The report doesn't need to be smart. It just needs to happen every day, without fail, until checking it becomes as automatic as checking email.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>projectmangement</category>
      <category>worklife</category>
    </item>
    <item>
      <title>I Modeled a Relational Database in Notion (No Code) to Kill My Spreadsheet Chaos</title>
      <dc:creator>VATSRAJ PARMAR</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:46:56 +0000</pubDate>
      <link>https://dev.to/vatsraj_parmar_17a32201fd/i-modeled-a-relational-database-in-notion-no-code-to-kill-my-spreadsheet-chaos-3e0k</link>
      <guid>https://dev.to/vatsraj_parmar_17a32201fd/i-modeled-a-relational-database-in-notion-no-code-to-kill-my-spreadsheet-chaos-3e0k</guid>
      <description>&lt;p&gt;As a developer, I default to building things — but not every problem needs a custom app. My client work was scattered across a spreadsheet for invoices, a doc for project notes, and a notes app for task lists. None of it talked to each other. Every status update meant updating three places by hand.&lt;/p&gt;

&lt;p&gt;The fix wasn't code. It was applying actual database thinking — foreign keys, relations, one-to-many — to a tool I'd been using like a glorified to-do list: Notion.&lt;/p&gt;

&lt;p&gt;The problem with flat lists&lt;/p&gt;

&lt;p&gt;Most people use Notion databases as flat tables. A "Clients" list. A separate "Projects" list. No connection between them. That's the Notion equivalent of denormalized data — duplicate info everywhere, no single source of truth.&lt;/p&gt;

&lt;p&gt;The fix is the same one you'd reach for in Postgres: relations.&lt;/p&gt;

&lt;p&gt;Modeling it like a schema&lt;/p&gt;

&lt;p&gt;I mapped out four entities, the same way I'd sketch a schema before writing migrations:&lt;/p&gt;

&lt;p&gt;Clients (1) ──&amp;lt; Projects (many)&lt;br&gt;
Projects (1) ──&amp;lt; Tasks (many)&lt;br&gt;
Clients (1) ──&amp;lt; Invoices (many)&lt;br&gt;
Projects (1) ──&amp;lt; Invoices (many)&lt;/p&gt;

&lt;p&gt;In Notion, this becomes relation properties — each database gets a field that points to rows in another database. Set it up as a two-way relation, and opening a client automatically shows every project, task, and invoice tied to them. No manual lookups, no copy-pasting a client name into three different sheets.&lt;/p&gt;

&lt;p&gt;The four databases&lt;/p&gt;

&lt;p&gt;Clients — contact info, status (Active/Paused/Past), industry. The "root" entity everything else relates back to.&lt;/p&gt;

&lt;p&gt;Projects — relates to Clients. Adds status, deadline, budget, priority. This is where a Kanban view (grouped by Status) actually earns its keep — dragging a card from "In Progress" to "Review" feels like a real workflow, not just checking a box.&lt;/p&gt;

&lt;p&gt;Tasks — relates to Projects (which relates to Clients, so you get the chain for free). Due dates, priority, assignee.&lt;/p&gt;

&lt;p&gt;Invoices &amp;amp; Payments — relates to both Clients and Projects. This was the one I initially got wrong — I first related it only to Projects, then realized I wanted to filter "everything owed by Client X" without going through the project layer. Two relations, no downside.&lt;/p&gt;

&lt;p&gt;Views matter as much as the schema&lt;/p&gt;

&lt;p&gt;Once the relations exist, the same underlying data supports wildly different views:&lt;/p&gt;

&lt;p&gt;Table — the full spreadsheet-style view, for bulk editing&lt;br&gt;
Board (Kanban) grouped by Status — for daily workflow&lt;br&gt;
Calendar grouped by Deadline — for "what's due this week"&lt;/p&gt;

&lt;p&gt;This is the part that took me longest to appreciate: the data model is the hard part. Once it's right, views are nearly free.&lt;/p&gt;

&lt;p&gt;Where this ended up&lt;/p&gt;

&lt;p&gt;I turned the finished structure into a template — four connected databases, pre-filled with sample data so the relations are visible immediately instead of staring at empty tables. If you want the ready-built version instead of wiring up the relations yourself, I packaged it here: &lt;a href="https://vatsraj.gumroad.com/l/jhpue" rel="noopener noreferrer"&gt;Client &amp;amp; Project Tracker — Notion Template.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Either way — if you're a freelancer or small agency juggling client info across too many tools, the fix probably isn't a new app. It's modeling the relations you already have data for.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>productivity</category>
      <category>freelance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building JWT Authentication + Role-Based Access Control for MERN Apps</title>
      <dc:creator>VATSRAJ PARMAR</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:00:13 +0000</pubDate>
      <link>https://dev.to/vatsraj_parmar_17a32201fd/building-jwt-authentication-role-based-access-control-for-mern-apps-fbc</link>
      <guid>https://dev.to/vatsraj_parmar_17a32201fd/building-jwt-authentication-role-based-access-control-for-mern-apps-fbc</guid>
      <description>&lt;p&gt;If you've built more than one or two production apps, you've probably written auth + RBAC from scratch more times than you'd like to admit. Here's the pattern I keep coming back to — JWT access + refresh tokens, httpOnly cookies, and role-based route protection on both the frontend and backend.&lt;/p&gt;

&lt;p&gt;This post walks through the core pieces. At the end I'll link a ready-to-use starter kit if you'd rather not rebuild this from scratch every time.&lt;/p&gt;

&lt;p&gt;The problem with "just use localStorage"&lt;/p&gt;

&lt;p&gt;A lot of tutorials store the JWT in localStorage and call it done. The problem: any XSS vulnerability on your site can read localStorage and steal the token. httpOnly cookies can't be read by JavaScript at all, which closes that attack surface.&lt;/p&gt;

&lt;p&gt;The tradeoff is a bit more setup — so let's go through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The User model, with roles baked in&lt;/strong&gt;&lt;br&gt;
`// models/User.js&lt;br&gt;
import mongoose from "mongoose";&lt;br&gt;
import bcrypt from "bcryptjs";&lt;/p&gt;

&lt;p&gt;export const ROLES = ["admin", "manager", "user"];&lt;/p&gt;

&lt;p&gt;const userSchema = new mongoose.Schema(&lt;br&gt;
  {&lt;br&gt;
    name: { type: String, required: true, trim: true },&lt;br&gt;
    email: { type: String, required: true, unique: true, lowercase: true },&lt;br&gt;
    password: { type: String, required: true, minlength: 8, select: false },&lt;br&gt;
    role: { type: String, enum: ROLES, default: "user" },&lt;br&gt;
    refreshTokenHash: { type: String, select: false },&lt;br&gt;
  },&lt;br&gt;
  { timestamps: true }&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;userSchema.index({ role: 1 });&lt;/p&gt;

&lt;p&gt;userSchema.pre("save", async function (next) {&lt;br&gt;
  if (!this.isModified("password")) return next();&lt;br&gt;
  this.password = await bcrypt.hash(this.password, 12);&lt;br&gt;
  next();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;userSchema.methods.comparePassword = function (candidate) {&lt;br&gt;
  return bcrypt.compare(candidate, this.password);&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export default mongoose.model("User", userSchema);`&lt;/p&gt;

&lt;p&gt;A couple of things worth noting:&lt;/p&gt;

&lt;p&gt;select: false on password and refreshTokenHash means these never come back in a normal .find() — you have to explicitly .select("+password") when you need them. One less way to accidentally leak a hash in an API response.&lt;br&gt;
The role field has an enum — Mongoose will reject anything outside your defined roles at the database level, not just in your app logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generating access + refresh tokens&lt;/strong&gt;&lt;br&gt;
`// utils/generateTokens.js&lt;br&gt;
import jwt from "jsonwebtoken";&lt;br&gt;
import crypto from "crypto";&lt;/p&gt;

&lt;p&gt;export const generateAccessToken = (user) =&amp;gt;&lt;br&gt;
  jwt.sign({ sub: user._id.toString(), role: user.role }, process.env.JWT_ACCESS_SECRET, {&lt;br&gt;
    expiresIn: "15m",&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;export const generateRefreshToken = (user) =&amp;gt;&lt;br&gt;
  jwt.sign(&lt;br&gt;
    { sub: user._id.toString(), tokenVersion: crypto.randomUUID() },&lt;br&gt;
    process.env.JWT_REFRESH_SECRET,&lt;br&gt;
    { expiresIn: "7d" }&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;export const cookieOptions = {&lt;br&gt;
  httpOnly: true,&lt;br&gt;
  secure: process.env.NODE_ENV === "production",&lt;br&gt;
  sameSite: "strict",&lt;br&gt;
  path: "/api/auth",&lt;br&gt;
  maxAge: 7 * 24 * 60 * 60 * 1000,&lt;br&gt;
};`&lt;/p&gt;

&lt;p&gt;Short-lived access token (15 min), long-lived refresh token (7 days), different secrets for each — if one leaks, the other stays valid independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The RBAC middleware itself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that actually enforces roles:&lt;br&gt;
&lt;code&gt;// middleware/roleMiddleware.js&lt;br&gt;
export const authorize = (...allowedRoles) =&amp;gt; {&lt;br&gt;
  return (req, res, next) =&amp;gt; {&lt;br&gt;
    if (!req.user) return res.status(401).json({ message: "Not authorized" });&lt;br&gt;
    if (!allowedRoles.includes(req.user.role)) {&lt;br&gt;
      return res.status(403).json({&lt;br&gt;
        message:&lt;/code&gt;Access denied. Requires role: ${allowedRoles.join(" or ")}&lt;code&gt;,&lt;br&gt;
      });&lt;br&gt;
    }&lt;br&gt;
    next();&lt;br&gt;
  };&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Usage:&lt;br&gt;
&lt;code&gt;router.get("/reports", protect, authorize("admin", "manager"), getReports);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The key design decision here: roles are checked server-side, always. Your React app can (and should) also hide admin-only UI from regular users, but that's a UX nicety — the actual security boundary is this middleware. Never trust a frontend route guard alone.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;4. Don't forget: never trust the client on registration either&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
This one's easy to miss. If your registration endpoint accepts a role field straight from the request body, anyone can sign up as an admin:&lt;/p&gt;

&lt;p&gt;`// DON'T do this:&lt;br&gt;
const user = await User.create({ name, email, password, role: req.body.role });&lt;/p&gt;

&lt;p&gt;// DO this instead — hardcode new signups to the lowest privilege:&lt;br&gt;
const safeRole = "user";&lt;br&gt;
const user = await User.create({ name, email, password, role: safeRole });`&lt;/p&gt;

&lt;p&gt;Promotion to admin/manager should only happen through a separate, already-authenticated admin-only endpoint — never at signup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Frontend route guards&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the React side, a simple wrapper component keeps unauthorized users out of role-gated pages:&lt;/p&gt;

&lt;p&gt;`// components/ProtectedRoute.jsx&lt;br&gt;
import { Navigate } from "react-router-dom";&lt;br&gt;
import { useAuth } from "../context/AuthContext";&lt;/p&gt;

&lt;p&gt;const ProtectedRoute = ({ children, roles }) =&amp;gt; {&lt;br&gt;
  const { user, loading } = useAuth();&lt;/p&gt;

&lt;p&gt;if (loading) return &lt;/p&gt;Loading...;&lt;br&gt;
  if (!user) return ;&lt;br&gt;
  if (roles &amp;amp;&amp;amp; !roles.includes(user.role)) return ;

&lt;p&gt;return children;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;export default ProtectedRoute;`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Route path="/admin" element={&lt;br&gt;
  &amp;lt;ProtectedRoute roles={["admin"]}&amp;gt;&lt;br&gt;
    &amp;lt;AdminPanel /&amp;gt;&lt;br&gt;
  &amp;lt;/ProtectedRoute&amp;gt;&lt;br&gt;
} /&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Again — this is UX, not security. The real gate is the backend middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Auto-refreshing expired access tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fifteen-minute access tokens mean your users would get logged out constantly without this piece. An axios interceptor handles it silently:&lt;br&gt;
&lt;code&gt;// utils/api.js&lt;br&gt;
api.interceptors.response.use(&lt;br&gt;
  (res) =&amp;gt; res,&lt;br&gt;
  async (error) =&amp;gt; {&lt;br&gt;
    const original = error.config;&lt;br&gt;
    if (error.response?.status === 401 &amp;amp;&amp;amp; !original._retry) {&lt;br&gt;
      original._retry = true;&lt;br&gt;
      const { data } = await axios.post("/api/auth/refresh", {}, { withCredentials: true });&lt;br&gt;
      setAccessToken(data.accessToken);&lt;br&gt;
      original.headers.Authorization =&lt;/code&gt;Bearer ${data.accessToken}&lt;code&gt;;&lt;br&gt;
      return api(original);&lt;br&gt;
    }&lt;br&gt;
    return Promise.reject(error);&lt;br&gt;
  }&lt;br&gt;
);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The refresh token lives in an httpOnly cookie, so this request automatically includes it — no manual token juggling on the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrapping up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the core of it: short-lived access tokens, httpOnly refresh tokens, server-enforced roles, and a frontend that stays in sync without constant re-logins.&lt;/p&gt;

&lt;p&gt;If you want the fully wired version of this — rate limiting, refresh token hashing in the DB, paginated admin user management, and a working React UI — I packaged it as a starter kit: &lt;a href="https://vatsraj.gumroad.com/l/uvvnww" rel="noopener noreferrer"&gt;MERN Auth + RBAC Starter Kit&lt;/a&gt;. Drop it in, connect your MongoDB URI, and you've got working auth in about 10 minutes instead of rebuilding all of this from scratch.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about any of the above in the comments.&lt;/p&gt;

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