Logistics infrastructure company
Components for Any Data
One component library that draws any customer's data, from the stream to the chart.
- Role
- Design Engineer
- Data model, interface, front end and server design on my own, with Claude Code and Cursor
- Outcome
- One library serves every customer. Each component declares the shape of data it needs, and customers connect their own streams by choosing fields.
What this shows
- Front-end architecture
- Data contracts
- Data-binding UX
The preview panel, rebuilt for this case study
The product is confidential, so this uses mock data and is for demonstration only.
| Category | Value |
|---|---|
| Leeds | 364 |
| Bristol | 190 |
| Glasgow | 160 |
| Cardiff | 122 |
| Norwich | 41 |
Try north_depot.scans: same meaning, different field names.
Rows as they arrive
First 3 of 1,840
| categorytown | valueparcels | datedelivered_on | textdriver_id |
|---|---|---|---|
| Leeds | 128 | 2026-09-28 | D-104 |
| Bristol | 102 | 2026-09-28 | D-221 |
| Glasgow | 87 | 2026-09-28 | D-318 |
Part 1: The model
Different shapes · A shape, not field names · One pipeline
Every customer's data has a different shape
Our platform carries customers' operational data as live streams, and customers build their own products on it, picking components from our component store. But the components only showed fixed values in their settings, so no customer could see what their own data would look like in one. One set of sample data couldn't cover every case either, as each customer names and formats the same data differently.
So I started from two questions: Can we connect live streams to the store's components? What does a component need to know about its data?
- category
- value
- no role
courier_app.deliveries
1,840 rows
- town (category)
- Leeds
- parcels (value)
- 128
- delivered_on
- 2026-09-28
- driver_id
- D-104
north_depot.scans
26,708 rows
- scan_id
- S-88120
- site (category)
- York
- qty (value)
- 34
- eta_ts
- 1790583600
partner_carrier.feed
412 rows
- Ref
- PC-2231
- City (category)
- Dundee
- ArrDate
- 30/09/2026
- Status
- Arrived
- no value
- no number field
A component promises a shape, not field names
I based the model on ECharts' dataset and encode: a chart type has fixed dimensions it draws, and encode maps the data's columns onto them. A contract adds what ECharts leaves out: each role's type, and which are required.
Each component declares one contract, and a stream can feed it once its fields map onto that contract's roles. Components that draw the same shape share one, so a bar chart, a donut, a treemap and a table all declare the same contract. Sample data fits it too, so a component explains itself before anything is connected.
Contracttotals-by-category
- categorytextrequired
- valuenumberrequired
Declared by
- Bar
- Donut
- Treemap
- Table
Sample datafits the same contract
// A contract names the roles a
// component draws, not the fields a
// stream happens to have.
export const totalsByCategory =
defineContract({
id: "totals-by-category",
roles: {
category: {
type: "text",
required: true,
},
value: {
type: "number",
required: true,
},
},
});
// Bar, donut, treemap and table all
// declare the same contract.
export const BarChart = component({
contract: totalsByCategory,
settings: { sort: "desc" },
});
One pipeline from stream to chart
An adapter turns each source system's format into flat rows, a binding maps the stream's fields onto the contract's roles, the contract checks their types, and the component draws the roles. Components never subscribe to streams, and settings hold configuration only, never data.
Charts and tables read a stream, filters and search read the page, and forms wait for the user.
Sourceits own format
{"evt":"delivered", "payload":{ "town":"Leeds", "parcels":"128"}}
Adapterflat rows
townLeeds
parcels128
Bindingfields onto roles
category← town
value← parcels
Contractroles and types
categorytext · required
valuenumber · required
Componentdraws the roles
- Charts, tablesread a stream
- Filters, searchread the page
- Formswait for the user
Part 2: The interface
Grouped by fit · Fields like chart axes
Streams grouped by fit, judged on the server
The first answer listed every stream by row count. A test organisation already had 74, and most didn't fit: users only found out after reading the rows.
I grouped the list by fit instead. The browser can't judge fit, as a stream record carries no field names, so I designed one endpoint on the server to judge it.
First answer · by row count
74 streams, one list
- telematics.pings
- gate_cameras.events
- north_depot.scans
- finance.invoices
- driver_shifts.log
- 69 more
Grouped by fit
Fits
- courier_app.deliveries
- north_depot.scans
Partly fits
- partner_carrier.feed
- driver_shifts.log
Other
Search 70 more streams
- PickerAsks which streams fit
- One endpointFits · Partly fits · Other
- Phase 1The newest entryIts field names, from a read the platform already makes.
- Phase 2A stored field profileIndexed by field: the same cost for 50 streams or 5,000.
Each field picked like a chart axis
The people connecting data aren't always engineers, so connecting fields can't feel like configuration. Each role has a dropdown of the stream's fields, the way you pick a chart's axes.
The store suggests a field for each role from aliases and industry terms. The stream list and the field suggestions share one matcher, so a stream marked "Fits" always fills its fields.
Bar chart
| Category | Value |
|---|---|
| Wakefield | 89 |
| Leeds North | 88 |
| York | 63 |
| Harrogate | 22 |
| Selby | 12 |
Part 3: Outcome
Takeaway
Takeaway
- The store couldn't preview a customer's data, which looked like an interface problem. The cause was in the data model, so I defined the shape each component needs before designing the panel.
- Judging fit on the server lets users see which streams fit before they pick, however many there are.
- Every layer has one job, so a new source system needs a new adapter, and nothing else changes.