Overview
Marketplace Portal is the client side of a private-markets marketplace platform, serving three distinct personas from the same codebase: Market Organisers who run a marketplace, Market Providers who list items and services on it, and Market Consumers who buy from it. It was TypeScript from day one, bootstrapped with Create React App's TS template, and grew into a large multi-persona SaaS product over the time I worked on it.
I was one of the top five contributors on a large team, working across the full breadth of the app — catalogue tables, dynamic forms, market management, and the ongoing tech-debt work that keeps a codebase this size workable.
The problem
A marketplace platform serving three different user types has to solve three different problems at once, in the same UI shell. Organisers need market configuration and category management. Providers need to list items against schemas that vary by category — a physical good has different required fields to a digital one, or a consulting service. Consumers need to browse, request quotes, and place orders. None of that maps cleanly onto a single component model, and the item catalogue in particular was a large, growing table that had outgrown client-side rendering.
What I built
Server-side pagination for the item catalogue
The item catalogue table was one of the largest and slowest-loading views in the app. It had been rendering the full dataset client-side, which stopped scaling as the catalogue grew. I moved it to server-side pagination — fetching pages on demand rather than the whole table up front, which meant reworking how filters, sorting, and page state interacted with the query layer rather than just adding a page control on top.
JSON-Schema-driven forms
Item, education, and RFQ (request for quote) forms were built on @rjsf (React JSON Schema Form) rather than hand-rolled per category. Different item categories — physical goods, digital items, services — have different required fields, and the schema-driven approach meant a new category didn't need a new form component, just a new schema. That included handling conditional schemas (fields that only apply to certain item types), respecting field ordering via x-order, and building preview forms so providers could see what a listing would look like before publishing.
Market management
Edit modals for market configuration, fixes to the category tree, and a "mark location as default / collection point" flow for providers with multiple locations. This is the unglamorous but necessary work of a marketplace product — the parts that don't show up in a demo but that providers rely on every day.
Sustained tech-debt work
A recurring thread through my time on this project was feature-flag removal — settings flags, app-refresh flags, RFQ-from-orders — the cleanup that happens after a feature has proven itself and the flag is just dead branching logic waiting to bite someone. Alongside that: console-error cleanup, accessibility and layout fixes (clipped grid titles, double-scroll modals, side-nav message counts not updating), and general codebase hygiene.
Testing
Heavy test authoring across the board — unit tests for families, reconciliations, and provider cards, and Cypress E2E coverage. I also worked on de-flaking and upgrading the Cypress suite itself, which matters more than it sounds: a flaky E2E suite trains a team to ignore red builds, and that's a habit worth avoiding.
Engineering decisions
Schema-driven forms over per-category components
The alternative to @rjsf was a form component per item category, which is the more obvious path when you're building the first category and the wrong one once you're building the fifth. Centralising form generation around JSON Schema meant new item types were a data change, not a code change — at the cost of some genuine complexity in getting conditional schemas and field ordering right. That tradeoff paid off as the number of categories grew.
Server-side pagination as a targeted fix, not a rewrite
Rather than a broader rearchitecture, the pagination work was scoped tightly to the catalogue table — fix the thing that was actually slow, in the place it was slow, without dragging in unrelated parts of the data layer. Scoped fixes like this are easier to review, easier to roll back, and don't block on unrelated decisions.
Stack
| Layer | Choice |
|---|---|
| Framework | React · TypeScript · Create React App |
| State | Redux + deox · React Query |
| UI | Material UI · styled-components |
| Forms | Formik + Yup · @rjsf (JSON Schema Form) |
| Data | react-table / react-virtualized |
| Maps | Mapbox GL |
| Auth | Amazon Cognito |
| Analytics | Amplitude |
| i18n | i18next / react-i18next |
| Testing | Cypress · Testing Library · Storybook |
Reflections
The schema-driven forms were the most interesting part of this project to work on, because the win wasn't visible in the UI at all — it was in how easy the next item category became to add. The pagination work was the opposite: a very visible fix (the table stopped being slow) built on a fairly mechanical change once the query layer was in place.
The tech-debt work — flag removal, de-flaking Cypress, chasing down console errors — doesn't make for an exciting changelog entry, but it's the work that keeps a large multi-persona app like this one from calcifying. A marketplace serving three personas from one codebase accumulates cruft fast if nobody's doing that work.