Note · Engineering
82k rows, no backend
How Shipyard IMS keeps around 82,000 allocations responsive in a browser tab with no server at all — and what that constraint teaches you.
Most people's instinct, when you say "an inventory system with about 82,000 allocation rows, FIFO costing, and milestone billing", is to reach for a database and a server. Shipyard IMS doesn't have either. It's a single self-contained web app — HTML, CSS and vanilla JavaScript with ExcelJS bundled in — that a shipyard runs in a Chromium tab. This note is about how that actually works, and why the zero-backend constraint made the engineering better rather than just cheaper.
Why no backend at all
The constraint wasn't ideological. The business lives in Excel, on machines that don't get new software provisioned on a whim, in an environment where "we'll stand up a server" means a procurement and IT conversation nobody wanted. A server would also have meant a second source of truth to reconcile against the spreadsheets people already trusted. So the design goal became: make the Excel workbook the source of truth, and make the app a very smart lens over it. No server means nothing to install, nothing to patch, nothing to get breached, and no sync problem — the data is a file the user owns.
The File System Access API is the whole trick
The piece that makes this viable is the File System Access API. Instead of the old download-a-copy dance, the app asks for a handle to the user's workbook once, then reads and writes that exact file directly. ExcelJS parses it into the in-memory model on open and serialises back to it on save. To the user it feels like a desktop app editing their own document — because functionally, it is. The cost of the trick is that it's Chromium-only, which for a known fleet of office machines is a non-issue, and which I'll come back to.
Keeping 82k rows fast: everything in memory, indexed
The core decision is that the entire working dataset lives in memory as plain JavaScript arrays and maps. No paging, no query layer, no IndexedDB round-trips in the hot path. That sounds reckless until you do the arithmetic: 82,000 rows of structured records is a few tens of megabytes, which is nothing for a modern laptop. What you get in return is that costing is synchronous — a FIFO consumption pass or a billing recalculation is just a loop over data that's already there, with no await in the middle to introduce a race or a half-updated view.
The performance work isn't in the storage, it's in not redoing work. Derived totals — per-hull costs, per-lot remaining quantities, un-billed balances — live in indexes that update incrementally as records change, instead of being recomputed by scanning all 82k rows on every edit. When you issue material from a lot, the app adjusts the two or three affected aggregates, not the whole book. That's the difference between an edit feeling instant and an edit janking for half a second.
The two rules that kept it sane
Two disciplines did most of the heavy lifting:
- The costing core never touches the DOM or the network. It's pure data-in, numbers-out. That makes it trivially testable and means the expensive logic isn't entangled with rendering. The UI is a separate layer that reads from the model and paints.
- Only render what's visible. You never build 82,000 table rows. The tables render the viewport and a small buffer, so DOM size stays constant regardless of dataset size. The browser is asked to lay out a few dozen rows, not tens of thousands.
Getting the big parse off the main thread
The one genuinely heavy operation is the initial import — parsing a large workbook. Done on the main thread, that freezes the UI for a few seconds and makes the app feel broken. Pushing the ExcelJS parse off the main thread keeps the interface alive and lets you show real progress instead of a locked tab. It's the classic lesson: users forgive a few seconds of clearly-communicated work far more than one second of an unresponsive page.
Durability without a database
"No backend" raises the obvious fear: what about crashes? The answer is layered. The canonical data is the Excel file on disk, so it survives the browser entirely. On top of that the app keeps a localStorage snapshot and rolling auto-backups, and every destructive action is undoable. A crash mid-edit costs seconds, not a day — and because the truth is a file the user can see and back up themselves, nobody has to trust an opaque server with their business.
Lessons learned
- In-memory is underrated. For datasets that comfortably fit in RAM, keeping everything in memory eliminates a whole category of async bugs and makes correctness-critical logic dramatically simpler to reason about. Reach for a database when you actually outgrow memory, not by reflex.
- Incremental beats fast. The win wasn't a faster full recompute — it was not doing the full recompute. Maintaining aggregates as data changes is more code than a naive scan, but it's the only thing that scales with edits.
- Pick your browser deliberately. Committing to Chromium bought the File System Access API and predictable memory headroom. Scoping the target platform up front turned "portable to everything, mediocre everywhere" into "excellent where it actually runs".
- Constraints as a feature. "No server" started as a limitation and ended as the selling point: nothing to install, nothing to approve, data the customer owns. The constraint shaped a better product.
Shipyard IMS is the flagship in a family of self-contained tools I build this way. If your team is running something important out of a spreadsheet and can't wait on an IT rollout, that's exactly the shape of problem this approach fits.