Wafer Wizards
Equipment maintenance tracking, designed from the lab floor
Solo build, 103 commits
- Commits
- 103
- Rewrite
- MERN → Next.js
- Data layer
- Raw SQL, no ORM
The problem
A piece of lab equipment goes down. One technician remembers it did this in March. Someone else recalls a firmware change that might be related. The fix, when it arrives, lives in a text message and a whiteboard that gets erased on Friday. Six months later the same tool fails the same way and the same afternoon is spent rediscovering the same answer.
I built Wafer Wizards because I had been on the wrong end of that loop often enough to know what the tool needed to be. Not a general-purpose ticket system — those exist and people avoid them — but something organized around equipment and its history, where logging an issue is fast enough to do while you are still standing in front of the machine.
The rewrite, and why it was worth it
The first version was a MERN app: React from create-react-app, an Express API, MongoDB. It worked. It also meant every page load was a spinner while the client asked the API for what it needed, and a growing pile of hand-written code to keep server and client agreeing about the shape of the data.
I rewrote it on Next.js with PostgreSQL. Three things drove the decision, and I want to be specific because “we rewrote it in the new framework” is usually a bad reason.
The data was relational. Equipment has issues, issues have updates and assignees, everything joins to everything. I had been hand-rolling those joins in application code against a document store. Postgres does that correctly and I stopped writing it.
The waterfall was structural, not a tuning problem. Rendering on the server let a page arrive with its data already in it, which removed both the spinner and the entire class of loading-state bugs underneath it.
Validation needed one source of truth. A Zod schema defines the shape once and both the form and the server action are checked against it. The previous version validated in two places and they drifted, which is how bad data got in. The status field is the clearest example — a fixed set of states, declared once:
const TicketSchema = z.object({
ticketID: z.string({ invalid_type_error: "Please select a ticket." }),
priority: z.coerce.number().gt(0, { message: "Priority should be 1, 2, 3...n" }),
description: z.string(),
assigned: z.string(),
status: z.enum([
"Pending", "In Work", "Paused",
"Under Review", "Complete", "Rejected",
]),
});There is no ORM here. Queries are raw SQL through the Postgres client, which I chose deliberately: the schema is small, the queries are the interesting part, and I wanted to be writing SQL rather than learning a query builder's opinion about SQL.
Authentication, and what you can actually see
Maintenance history is internal, so the application sits behind NextAuth with credential sign-in, hashed passwords, and middleware-enforced session checks on protected routes.
The deployed link shows the page describing the tool; the tracker itself sits behind sign-in. The source is public if you want to read how it works, and I can walk through the running application on a call.
Why the domain knowledge mattered
I did not pick this off a list of portfolio ideas. I picked a problem I had personally lost afternoons to, which meant I already knew the requirements that usually get discovered late: that logging has to happen while you are still in front of the machine, that history is organized by equipment rather than by ticket, and that anything requiring a second person to be available will not get used.