Acelab · Material Hub
Bulk Vendor RFIs: a workflow that runs between two companies
A workflow for architects to ask many manufacturers for product details at once, straight from their schedule; manufacturers answer in a grid that autosaves, and the answers land back in the schedule live. I led the frontend and wrote .NET endpoints for its paging, cancellation and live updates. It merged behind a feature flag.
- Problem
- Architects ask manufacturers for product details across a company boundary, so the workflow has to handle drafts, deadlines, permissions on both sides, autosaving answers and status that stays live in every open view.
- My part
- I led the frontend, built the architect’s send wizard and the manufacturer’s inbox and response grid, and was the second contributor to the workflow’s .NET backend, which a teammate led.
- Result
- Merged behind a feature flag in 2026, then extended through October with cancellation, deadline extensions and multi-cell sending; Acelab markets it as Bulk Vendor RFIs.
The problem
In Acelab’s Material Hub, architects build product schedules: spreadsheet-style lists of the products, materials and finishes in a drawing set. When a schedule needs details only a manufacturer has, the architect sends a request for information, an RFI.
That request crosses a company boundary. One company asks, another owes an answer by a deadline, and Acelab staff pick up whatever reaches no one. Drafts outlive a session, colleagues share the answering, deadlines pass and requests get withdrawn. The answer has to land in the architect’s schedule, and every open screen has to agree on where things stand.
What I built
On the architect’s side, I built the send wizard. The architect selects rows in the schedule or, later, a range of cells that also pre-selects the fields it covers, then moves through a five-step wizard: products, fields, recipients and deadline, message, review. Reopening it on the same selection resumes the saved draft. The API creates one numbered RFI per manufacturer; any that reach no manufacturer contact fall back to Acelab staff, who route them on a board a teammate built.
Each manufacturer sees the request in an inbox scoped to their brand and answers in a grid built to behave like the schedule: a real HTML table with a sticky header and product column, the schedule’s own read-only cells, an editor for each data type, arrow-key movement and type-to-edit. Answers autosave. Colleagues can be assigned to share the work, viewers can only read, and submitting locks the response. Try a rebuild of it below.
| Product | Finish | Color | Lead time (weeks) |
|---|---|---|---|
| PT1 Porcelain field tile | Matte | — | 6 |
| ACT2 Acoustic ceiling panel | — | White | — |
| QZ1 Quartz countertop | Polished | — | 8 |
| WV3 Wood veneer wall panel | — | Natural oak | — |
| CPT1 Carpet tile | Loop pile | Graphite | 4 |
Press “Show conflicting saves” to watch two edits race, or edit any cell yourself.
- Each save appears here as it’s sent and as it returns.
On submit, the server writes the answers into the architect’s schedule and broadcasts on the project’s real-time channel, so an open schedule updates without a reload. A marker on each requested cell shows when it is out for an RFI, then whether the manufacturer supplied the value or left it as it was; if the architect later types over an answer, the server withdraws the credit and the marker updates live. Each marker is a labeled, keyboard-operable button that opens the RFI in a drawer.
A response locks once its deadline passes. A due-soon chip appears in the three days before, and the manufacturer can ask for more time without first missing the deadline. The server decides what counts as due soon, on US Eastern dates, and the client counts days in the same zone, so the chip and the flag can’t disagree.
On the .NET side, I wrote multi-colleague assignment, reopen requests, server-side paging, per-cell RFI state with its real-time broadcasts, the cancel and a dashboard summary. I limited Send RFI to members of the project’s company: the button hides on a definite mismatch, and the API enforces the rule on its own, failing closed when ownership can’t be resolved. Before the first version merged, I traced each spec requirement to the code and found ten acceptance criteria the build had missed; six shipped within two days, and the rest were deferred or descoped.
Decisions
Decision
Autosave every answer, and never let a late save overwrite a newer edit.
- Why
- A manufacturer’s draft had to survive a reload, so each cell saves on its own shortly after an edit. Saves can return out of order, so a returning save marks only the cells whose value still matches what it sent.
- Cost
- Each cell has three values to keep straight: on screen, in flight and saved. An empty cell is ambiguous (never answered, or answered then cleared), so cells carry an explicit saved-on-the-server flag. Review still caught a cleared cell bringing back the architect’s faded reference value; that was fixed before merge.
Decision
Filter, sort, count and page on the server.
- Instead of
- Loading every product and every cell when a response opened.
- Why
- So a large RFI opens fast. The response view loads the project details, attachments and the first page of products; the rest arrive as you scroll, into virtualized rows.
- Cost
- Sorting is limited to what the API supports, so when I later added a manufacturer column to the staff board, I left it unsortable rather than ship a client-side sort that would behave differently on each page. Once RFIs could be canceled, paging from the local row count also skipped one, so I switched “load more” to offset by what the server still held.
Decision
Cancel in one transaction, and announce it only after the commit.
- Why
- As I first built it, an architect could withdraw an RFI until the manufacturer or Acelab staff opened it. The cancel claimed the row, snapshotted what hung off it and deleted it in one transaction. Only after the commit did it notify the manufacturer and clear the RFI from every open board, inbox, schedule cell and conversation, over real-time channels authorized per persona.
- Cost
- Every writer had to expect a row to vanish: opening, submitting and assigning were changed to refuse a canceled RFI rather than report success. I also stopped skipping real-time events the current user had triggered, since their other tabs kept showing an RFI they had just canceled.
What I’d change
List every reader before deleting. Canceling removed the RFI and the conversation that existed only to host it, which stranded a manufacturer reading that thread on a dead page and made resending the same selection create nothing, until each was fixed. Next time I would list every reader of a row before shipping its delete. In October 2026 a teammate reworked cancel to keep the RFI with a Canceled status.
Hold UI fixes for a browser pass. One bug survived a fully passing suite because the grid’s tests injected a value the store had stopped producing. I check new tests by breaking the code they cover, but a few later fixes still merged on unit tests alone.
Outcome
The workflow takes an RFI from the architect’s schedule to the manufacturer and writes the answers back into the schedule, live. The first version merged behind a feature flag; cancellation, deadline extensions and multi-cell sending followed through October 2026, and Acelab markets it as Bulk Vendor RFIs (opens in a new tab). What transfers: the server decides who may act and what a deadline means, and a late save never overwrites a newer edit.
What’s left out
Acelab’s code and data are private, so the diagrams are redrawn and any interactive demo is a rebuild with made-up data. Names of colleagues, customers and internal tools are left out on purpose. I’m glad to go deeper on any of it in an interview.