Case study / Vol. 01

The whole system, in one session.

WorkShield is an employee management system with role-based access control. The constraint was speed: design, build, test, and deploy in one focused session.

Time
One session
Surface
11 endpoints · 6 pages
Roles
3 (Admin · HR · Employee)
Database
MongoDB on local Docker
Auth
JWT bearer · 7-day expiry
Tests
Playwright E2E + API contract
01

The problem

Permissions everywhere, trust nowhere.

Internal tools accumulate role complexity faster than they accumulate features. An HR manager needs to approve leaves, but not delete employees. An employee needs to see their pay, but not their colleague's. An admin needs to do everything — without accidentally exposing salary data through a leaky endpoint.

The wrong shape is to scatter if (user.role === "admin") across handlers. The right shape is two composed middlewares — one that asserts the role, one that asserts ownership — and to make them impossible to bypass.

02

Decisions

Five opinionated choices.

DEC 01

Enforce RBAC at the API, not the UI.

Hiding routes and buttons feels safe. It isn't. Every protected endpoint runs requireAuth + requireRole, plus an ownership check that rejects access to other people's records — even for an Admin reading sensitive HR fields.

Why: Frontend hiding is cosmetic. A motivated user can hit /api/employees with curl. Backend is the only honest gate.

DEC 02

Pick JWT over sessions.

Stateless bearer tokens via jose, signed with a 32+ char secret, 7-day expiry. No session table. No sticky sessions. Trades server-side revocation for horizontal scale and operational simplicity.

Why: For a demo with a single admin tier, revocation hooks aren't worth the storage and join cost. If we needed to invalidate, a denylist in Redis would be the next step.

DEC 03

Mongo over Postgres.

Mongoose models map cleanly to the four entities (User, Employee, Leave, Salary). No relational joins, no migrations, no schema drift on demos. Mongo's flexible schema absorbs feature additions without a migration step.

Why: Postgres wins for analytics and constraints. Mongo wins for velocity on schemas that change weekly. This system trades that flexibility on purpose.

DEC 04

Bun over Node.

Bun.password.hash replaces bcrypt. Bun.serve replaces express. Native TypeScript. Fast install. Single binary for dev + prod. The whole runtime story collapses into one tool.

Why: On a small team, the productivity wins (no node-gyp, no ts-node) outweigh the smaller ecosystem and missing edge-case packages.

DEC 05

No state library.

Just useState + a tiny AuthProvider context. Zustand and Redux are overkill for four pages. Server state (employees, leaves, salary) refetches per-page. Optimistic UI on critical writes.

Why: State libraries pay off when you share state across many components. This dashboard has none of that. Adding a library would be ceremony tax.

03

Architecture

How a request flows.

Request flow: client → JWT → role guard → ownership check → MongoCLIENTReactAUTHverifyTokenROLE GUARDrequireRoleOWNERSHIPuserId matchMONGOMongooseREQUEST FLOW · GET /api/employees/:idREJECTIONS:401 missing/invalid token · 403 wrong role · 403 not your record · 404 not found
04

Build phases

Six phases. One session.

PHASE 01

Schema

10 min

Mongoose models for User/Employee/Leave/Salary.

PHASE 02

Auth + RBAC

20 min

JWT signing, password hashing, requireAuth, requireRole, ownership checks.

PHASE 03

API surface

30 min

Hono routes for auth, employees, leaves, salary. Zod validation on every body.

PHASE 04

Dashboard

60 min

Vite + React 19, react-router, sidebar layout, modal CRUD, role-aware nav.

PHASE 05

E2E tests

20 min

Playwright covering login, RBAC matrix, employee CRUD, leave flow, direct API blocks.

PHASE 06

Seed + deploy

15 min

Idempotent seed on first boot. Docker compose for local. Render staging blueprint.

05

Outcomes

What shipped.

METRIC

100%

Type-safe routes
METRIC

API layer

RBAC enforcement
METRIC

5

E2E flows covered
METRIC

<800ms

First contentful paint
METRIC

~260KB

Bundle size (dashboard)
METRIC

Render + VPS

Deploy targets
06

If we kept going

Next moves.

Open the dashboard.

The system is live with seed data. Three roles preloaded — sign in to feel the difference between Admin, HR, and Employee.