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
}
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 }
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
Usertable. - 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
Useris the person.
TheProfilesare 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 |
+----------------+
⚙️ 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[]
}
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.
- ✅ Starts as a Customer → creates a
CustomerProfile - 🏪 Opens her store → creates a
VendorProfile - ⚙️ 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)