DEV Community

Cover image for 🎭 Designing a User Model for Multiple Roles (Without Losing Your Mind)
Abdul Basit Muhyideen
Abdul Basit Muhyideen

Posted on

🎭 Designing a User Model for Multiple Roles (Without Losing Your Mind)

You know that moment when your app suddenly needs more kinds of users?

You start with a simple User model — just name, email, password.
Life is peaceful. Everything makes sense.

Then your PM drops the bomb:

“We need admins, vendors, and customers.
And some users can be all three.”

Now you’re staring at your schema thinking,

“Maybe it’s time to switch careers.” 😅

Don’t panic — we’ve all been there.

In this post, we’ll design a clean, scalable, multi-role user system — the kind that won’t collapse the moment your startup adds a new user type.

We’ll explore:

  • The bad ideas (you’ve probably written before)
  • The better ideas
  • And finally, the best structure — one user table + separate profile tables.

🧱 1. The One-Table Disaster

Most apps start like this:

User {
  id: string
  name: string
  email: string
  password: string
  role: 'admin' | 'vendor' | 'customer'
  storeName?: string
  walletBalance?: number
}
Enter fullscreen mode Exit fullscreen mode

It looks fine — until reality hits.

  • Customers don’t have storeName
  • Vendors don’t have walletBalance
  • Admins don’t need half the columns

Now your table looks like a junk drawer full of nullable fields.
Welcome to the God Table Anti-Pattern — one table trying to be everything, and doing nothing well.


🪓 2. The “Split Everything” Overcorrection

Then comes the overcorrection phase:

Admin { id, name, email, password, permissions }
Vendor { id, name, email, password, storeName }
Customer { id, name, email, password, walletBalance }
Enter fullscreen mode Exit fullscreen mode

Looks cleaner, right?
Until you realize you just broke your system in three places.

Now you have:

  • 3 login routes
  • 3 sets of authentication logic
  • 3 password reset flows

And worst of all:
A user can’t be both a vendor and a customer without two separate accounts. 🤦‍♂️

That’s not role-based — that’s multiple-account chaos.


💡 3. The Smarter Way — “User + Profiles”

Here’s the scalable approach:

  • Keep shared user data (like email, password, name) in a single User table.
  • Create separate tables for each role’s profile (AdminProfile, VendorProfile, CustomerProfile).
  • Let one user have multiple profiles linked to them.

Think of it like this:

The User is the person.
The Profiles are the hats they wear.

So, Sarah can:

  • Log in once ✅
  • Buy products as a customer 🛒
  • Sell items as a vendor 🏪
  • Manage users as an admin ⚙️

All through one account.


🧭 4. Visualizing It

          +------------------+
          |      User        |
          |------------------|
          | id               |
          | name             |
          | email            |
          | passwordHash     |
          +--------+---------+
                   |
          +--------+---------+
          |                  |
  +----------------+  +----------------+
  | VendorProfile  |  | CustomerProfile|
  |----------------|  |----------------|
  | id             |  | id             |
  | userId (FK)    |  | userId (FK)    |
  | storeName      |  | walletBalance  |
  | businessType   |  | preferences    |
  +----------------+  +----------------+

          +----------------+
          |  AdminProfile  |
          |----------------|
          | id             |
          | userId (FK)    |
          | permissions[]  |
          | accessLevel    |
          +----------------+
Enter fullscreen mode Exit fullscreen mode

⚙️ 5. Schema Example (Prisma-style)

Here’s what that looks like in a clean ORM setup:

model User {
  id        String @id @default(uuid())
  name      String
  email     String @unique
  password  String
  createdAt DateTime @default(now())

  adminProfile    AdminProfile?
  vendorProfile   VendorProfile?
  customerProfile CustomerProfile?
}

model AdminProfile {
  id           String @id @default(uuid())
  userId       String @unique
  user         User   @relation(fields: [userId], references: [id])
  permissions  String[]
  accessLevel  String
}

model VendorProfile {
  id           String @id @default(uuid())
  userId       String @unique
  user         User   @relation(fields: [userId], references: [id])
  storeName    String
  businessType String
}

model CustomerProfile {
  id            String @id @default(uuid())
  userId        String @unique
  user          User   @relation(fields: [userId], references: [id])
  walletBalance Float
  preferences   String[]
}
Enter fullscreen mode Exit fullscreen mode

It’s modular, readable, and future-proof.
Adding a new role later? Just create a new profile table. No migrations. No mess.


🧠 6. Real-World Example

Meet Sarah.
She signs up on your platform.

  1. ✅ Starts as a Customer → creates a CustomerProfile
  2. 🏪 Opens her store → creates a VendorProfile
  3. ⚙️ Joins your team → adds an AdminProfile

Still one login, one token, one user record.
She just switches context — your backend handles the rest.


🔍 7. Why This Wins

Separation of concerns — shared auth, separate business logic.
Extensible — new roles = new tables, not new headaches.
Clean authorization — role checks happen on profile level.
Reusable auth — login once, access multiple contexts.
No null fields — because every role owns its own schema.

🚀 8. Wrapping Up

Good system design is like clean UI — invisible when done right.

This User + Separate Profile Tables approach gives you:

  • Flexibility
  • Scalability
  • And peace of mind 😌

As your app grows, you’ll thank yourself for choosing this pattern.

Because the next time someone says:

“We’re adding moderators now…”

You’ll just smile and create a ModeratorProfile table. 😎


💬 Coming Next

In the next article in this series:

Role-Based Access Control (RBAC) with User Profiles
— How to attach permissions dynamically and secure routes in your backend elegantly.

Top comments (0)