← All posts
Wednesday, April 22, 2026·4 min read

One login, three views: workers, trainers, and students share an app

designauth

At our test barn, our most common “edge case” turned out to be the most common case. The barn manager is also a trainer. A worker who mucks stalls also takes weekly lessons. The trainer's daughter rides in lesson programs and works on weekends. We had a roster of people whose job titles literally didn't fit in one column.

Our first version forced a choice — pick a primary role at signup, see the dashboard for that role, get told to ask the admin if you needed something else. It was wrong. Here's how we re-modeled it.

Primary role + extra roles

Each person has one OrgMember record per barn they belong to. That record has a primary role — typically owner, manager, employee, or worker — and an array of extra roles stored as JSON. The extras we use today are trainer and student, though there's nothing stopping us from adding more (vet, contractor, parent) later.

The primary role decides which dashboard you land on by default. The extras decide which additional sidebar links and routes you see. So a worker with the student extra role sees the worker dashboard and a “🏇 My Lessons” link in their sidebar — clicking it routes to the student portal at /lessons, which checks the same extras when deciding whether to render.

Authorization is one source of truth

Every protected route asks the same question: does this user's primary role match, OR does their extras include the right tag? The trainer dashboard at /trainer allows admins (primary role owner or manager) or users with trainer in extras. The student portal allows admins or users with student in extras. The lesson billing API uses the same check.

The reason it works cleanly is that authorization sits on the JWT session — extras are populated at login and travel with the request. We don't hit the database on every request to check “is this person a trainer?” — the answer is already in the token.

The migration that wasn't

When we built the lesson system, every WooCommerce student we imported needed the student role added. That was one line in the import script — we updated the OrgMember row's extras JSON with JSON.stringify([...current, "student"]) if it wasn't already present. Twenty-three rows updated, no schema change needed.

Same thing when we promote a worker to also handle lessons. We grant the trainer extra and that's it — they immediately see the trainer routes in the sidebar. No new account, no password reset, no “contact your admin”.

Why this matters

Modeling reality matters. People at a barn wear multiple hats — that's how horse operations have always worked. Software that pretends otherwise either forces people into duplicate accounts they forget, or limits the platform to a narrow slice of the operation. The role-overlay model means the platform can grow with someone's responsibilities instead of fighting them.

It's also fast to add. Want to support a vet who logs into the same app to record health events? Add a vet extra role and a route that checks for it. The auth, sidebar, and session plumbing are already there.

More posts
Migrating live lesson subscriptions from WooCommerce to Stripe in one afternoon
A trainer in our barn had been billing lessons through WooCommerce Subscriptions for years. Here's how we moved her live customer base into our system without making a single student re-enter their card.
What this platform is, and why we built it
A working barn runs on a stack of sticky notes, group texts, spreadsheets, and a payment processor or two. We replaced all of that with one app — here's what it does.
How gamification fixes stall cleaning quality, not just speed
Most barn-work apps that gamify cleaning end up rewarding the fastest, sloppiest worker. Here's the small change to the workflow that makes quality matter as much as speed.