Case study 05 · Permissions and real time
LSOE CRM
A role-based CRM for a student consultancy: five permission tiers over the same records, real-time collaboration so counsellors do not overwrite each other, and bulk import that deduplicates leads on the way in.
- Client
- Student consultancy
- Role
- Full-stack developer
- Roles modelled
- 5 permission tiers
- Stack
- Node.js, Express, React, MongoDB, Socket.IO

01The problem
A consultancy team worked leads across spreadsheets that were mailed around, so two counsellors regularly called the same student and nobody could tell which version of a record was current. Leads also arrived in bulk from partners as CSV and XLSX exports, full of duplicates.
The system had to give each tier of staff a different slice of the same data, keep everyone's view live, and absorb messy imports without creating a second copy of every student.
02Architecture
React with Redux Toolkit on the client, Node.js and Express over MongoDB behind it, Socket.IO carrying record-level events. Permissions are resolved server-side per request; the client only decides what to draw. Files and mail go through AWS S3 and SES, and the app is served through Nginx with PM2 keeping the processes up.
- Client
- React with Redux Toolkit; the UI renders what the server says the role may see.
- API
- Express middleware resolving per-route permissions from the user's tier.
- Real time
- Socket.IO rooms per record, broadcasting updates and edit presence.
- Import
- CSV and XLSX parsing with deduplication before anything is written.
- Operations
- AWS S3 and SES, Nginx, PM2.
03Decisions
- 01
Permissions on the server, never in the client
Each of the five tiers has its own view of a student record. The API decides what a request may read and write, and the client is treated as untrusted display. Hiding a button is never the control.
Trade-off More endpoints and more test cases; the client sometimes asks twice to know what it can render.
- 02
Socket.IO rooms per record, not a global channel
Counsellors joining a student record enter a room and receive only that record's events, with presence so they can see who else is in it. A single broadcast channel would have flooded every client as the team grew.
Trade-off Room lifecycle and reconnection handling became real work, especially on flaky connections.
- 03
Deduplicate at import, not afterwards
Partner exports overlap heavily. Imports are matched against existing records on the way in and merged rather than appended, so nobody has to clean up duplicates later.
Trade-off Import runs slower, and the matching rules need agreement with the business before they can be trusted.
04Proof
- 5
- Role tiers on one dataset
- 2
- Bulk formats ingested (CSV, XLSX)
- 0
- Spreadsheets left in the workflow
NextTalk
Internal tools deserve real architecture too.
If your team is still coordinating over spreadsheets, there is a system waiting to be built.