Skip to content
WorkAboutFAQContacthello@rajanna.dev
Case 04 / DOCUMENTS

DocFort

a drawing vault for LHP Motors

A revision-controlled vault for engineering drawings, built so one motor manufacturer stops sending outdated PDFs to the shop floor.

  • Role Sole developer (backend, frontend, desktop, deployment)
  • Timeline 2024, ~6 months
  • Status In production at LHP Motors, Solapur
  • Repo
  • In production since 2024
  • 3 app surfaces from one API — web PWA, Electron desktop, Express backend
  • 3-channel notifications (in-app, email, push) on a Redis-backed retry queue
Upload revision modal with revision type, reason, and disposal action fields
Fig 01Upload Drawing Revision modal for MBH-002 showing the revision as a structured domain object: Revision Type (Minor), Reason for Change (Standardization from a fixed vocabulary), and Disposal Action fields for pipeline and finished goods. Drawing Info and Preview panels alongside.

The world before this

LHP Motors makes motors. Making a motor means hundreds of component drawings, and every drawing gets revised — a tolerance changes, a supplier changes, a mistake gets caught. Before DocFort there was no system holding any of it. Drawings lived in folders and inboxes, and the question that mattered — is this the current revision? — had no reliable answer.

I built this alongside a GAD builder tool I was already delivering to them. They asked for a place to keep component drawings that would guarantee the version on an engineer's screen was the version the company had actually approved. Not a file server. A store that knew what a revision was.

I was the only person on it. Six months, every layer.

Drawing detail page with revision history showing type and reason tags
Fig 02Drawing detail page for MBH-002 showing Drawing Information (issue, revision, total revisions) and Revision History with Rev 00 tagged as minor and Standardization. View PDF, Details, and Download actions per revision. Metadata and Notify To sidebar on the right.

What made it hard

Three things fought me the whole way, and none of them were "build a CRUD app."

The first was that a revision isn't a file. It's a file plus a story: why it changed, whether it's minor or major, what happens to the stock that was made against the old one. The second was reach — a notification that a drawing changed is useless if it doesn't arrive, and "arrive" had to mean an engineer at their desk and an engineer who'd closed the app. The third was getting the current file onto machines across the plant without asking people to go hunting for it.

Green toast notification announcing new drawing created in real time
Fig 03Drawings list page with a green in-app toast notification in the top-right corner: 'New Drawing Created — A new drawing MBH-002 has been created with initial revision.' with a View button. Notification bell shows a live unread badge.

Modeling a revision as more than a file

The situation. Engineers don't just want the latest PDF. When a drawing changes, downstream people need to know why — is this a standardization tweak or a "we specified it wrong" correction — and what to do with parts already in the pipeline.

The decision. I modeled the revision as a first-class domain object, not a file attachment. Each revision carries a revisionType (minor or major), a reasonForChange drawn from a fixed vocabulary the engineers actually use — standardization, quality improvement, product development, wrongly specified, customer requirement — and a disposalAction describing what happens to pipeline, finished goods, and stock. When someone picks "wrongly specified," the form forces a corrective action before it'll save.

Why, and what I rejected. I could have stored a version number and a comment box and called it done. That's what a file server gives you. But a free-text comment is unsearchable and unaccountable, and the whole point was accountability. Conditional validation lives in Mongoose pre-validate hooks so the rules hold no matter which surface the data comes in from. The creation date is immutable — you can't backdate a revision.

What it produced. Every change to every drawing has a structured, queryable record of who, when, and why. The revision references its author polymorphically — a createdByModel of either User or Admin — so admin-made corrections and engineer submissions live in the same history without a fake user account.

Notification inbox dropdown showing two drawing events with priority labels
Fig 04Notification dropdown panel over the Drawings list showing two drawing-created events: MBH-002 marked unread with 'just now' timestamp and MBH-001 from 13 hours ago. Both carry Action Required and medium priority labels with a View all link.

Getting the notification to actually land

The situation. A drawing revision that nobody sees is worse than useless — someone keeps building against the old one, believing they're current.

The decision. I sent every important event down three channels: in-app (Socket.IO), email (Nodemailer), and browser/desktop push (web-push, with Firebase on the mobile side). All of it runs through a Redis-backed Bull queue with retry attempts and a fallback-to-email path, so a transient failure on one channel doesn't silently drop the message.

Why, and what I rejected. A single Socket.IO broadcast would've been simpler and I'd have shipped it in a day. It also would've reached exactly the people who happened to have the tab open. The queue was the harder call — more moving parts, Redis to run — but reliability was the actual requirement, not realtime alone. The NotificationManager takes per-send options for which channels to use and what priority, so not every event screams at everyone.

What it produced. When a revision goes up, the people who need it get told, on whatever channel reaches them, and the send survives a hiccup instead of vanishing.

Home dashboard with quick access cards and recent drawing updates
Fig 05Home screen for a Department Head user showing Quick Access cards for Drawings, Technical Data Sheets, and Performance Curves. Recent Updates lists drawing MBH-001 with its author and revision. Department Head Tools section links to the DH Dashboard.

Onboarding through an approval gate, not a signup form

The situation. This is internal engineering data. You can't let anyone who finds the URL make an account.

The decision. Registration is a request, not a signup. A new user submits their details — employee ID, department, designation, who they report to — and the account sits in a pending state until it's approved. The user model tracks approval status and keeps a status log of every state change with a timestamp and who made it. Access is role-based from there.

Why, and what I rejected. Open self-serve signup was off the table for obvious reasons. I could've handled approvals over email and created accounts by hand, but that doesn't scale past the first week. Putting the workflow in the data — with the reporting hierarchy captured on the user record — meant approvals had context instead of being a yes/no in someone's inbox. The tricky part was keeping this simple on screen while the backend carried a real state machine underneath.

What it produced. A join is a two-step, auditable process, and every account has a department, a role, and a paper trail of how it got approved.

Create drawing modal with live preview panel and next steps guide
Fig 06Create Drawing modal in dark mode showing the Drawing Information form with Code MBH-002, Issue 01, Revision 00, and description. A live Preview panel on the right reflects all inputs alongside a Notify Users field and file size. Next Steps guide the three-stage workflow.

Pushing the current file to the plant

The drawings arrive as PDFs, but a PDF is a clumsy thing to browse in a library view. So uploads run through a middleware pipeline that converts them to SVG and optimizes with SVGO before storage, which makes the web viewer fast and lets me export back to PDF client-side when someone needs the file. The desktop side is an Electron app with electron-updater and a file watcher, so the machine on the shop floor stays current without anyone manually downloading anything.

Admin dashboard showing user approval stats and approved user list
Fig 07Admin Dashboard showing user management: stat cards for Total Users, Pending Approval, Approved Users, and Admins. All Users tab lists Rajesh Adeli with Designer role, Design department, Department Head designation, and Approved status badge.

Under the hood

One Express/TypeScript API backs three surfaces: a React 19 PWA (Vite, Tailwind, Radix, Zustand), an Electron desktop client, and the mobile-facing push layer. Data is MongoDB through Mongoose, across six models — user, admin, component, revision, notification, push subscription. Auth is JWT with bcrypt over HTTP-only cookies, and access is role-based with a department/reporting hierarchy on the user record. Realtime is Socket.IO; reliable delivery is Bull on Redis. Uploads go through Multer and the PDF-to-SVG/SVGO pipeline. It's deployed with Docker and a GitHub Actions pipeline to EC2.

What shipped

It's live at LHP Motors in Solapur and is, per the team there, used daily by their engineering staff — the notes I'm working from put that number above 500 people relying on it for current drawings. I can vouch for the system being in production; the exact daily-active count is theirs to confirm, and I've flagged it below rather than dress it up. What the code demonstrates on its own: a real domain model for revisions, a delivery pipeline built for reliability over cleverness, and three client surfaces served from a single API by one developer.

What I'd do differently

I'd tighten the data model before I tightened the UI. A couple of the revision fields are typed as Mixed with the real rules enforced in pre-validate hooks — it works, but the schema doesn't tell you the truth about itself, and a stricter typed shape would've caught edge cases at the boundary instead of in a hook. I also leaned on Schema.Types.Mixed for user preferences early to move fast; I'd formalize that now. It shipped and it holds. I just know where the seams are.