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)