Component libraries scale poorly when every product has different domain models. We rebuilt Enravo's UI layer around schema definitions — and shipped admin panels in days instead of weeks.
Three years ago, the Rakton admin panel had 412 React components. Two engineers spent most of their time updating tables, forms, and detail pages whenever a column changed in the database. The components weren't bad — they were just downstream of every schema decision, and schema decisions were constant.
We stepped back and asked: why are we writing UI code at all for data that's already described in our schema definitions? A new field added to the Order schema means a new column in the table, a new input in the form, a new row in the detail view. None of that requires creative work — it's mechanical mapping. So we made the machine do it.
Most component libraries solve the wrong problem. They give you well-designed primitives — buttons, inputs, dialogs — and then leave the composition entirely to you. Every product team rebuilds the same structures: a table that queries an endpoint, supports sort and filter, renders cells based on column type, paginates. Multiply that by Orders, Products, Customers, Invoices, Branches, Users, Roles — and you have a hundred slightly-different table implementations.
We watched this happen at scale. Every Rakton module — POS, ERP, Delivery — started with a fresh table component because the previous one had been customized in ways that didn't generalize. The component library wasn't the problem. The problem was that we were composing UI by hand when the composition rules were already encoded in schema metadata.
Enravo Core's Schema Engine already defines every entity declaratively. An Order schema specifies its fields, types, validations, relations, indexes, and permissions. The API engine reads this schema to generate CRUD endpoints. The UI Engine reads the same schema to generate the views.
entity: Order
fields:
- name: id
type: uuid
primary: true
- name: customer
type: relation
target: Customer
display: customer.name
- name: total
type: money
currency: TRY
sortable: true
- name: status
type: enum
values: [pending, paid, shipped, delivered, cancelled]
color:
pending: amber
paid: blue
shipped: violet
delivered: green
cancelled: red
- name: createdAt
type: timestamp
sortable: true
default: now()
views:
table:
columns: [id, customer, status, total, createdAt]
defaultSort: createdAt:desc
filters: [status, createdAt]
detail:
sections:
- title: Order
fields: [id, customer, status, total, createdAt]
- title: Items
relation: orderItems
view: tableThat YAML — about 30 lines — generates the order list page, the filter sidebar, the detail page, and the relation panel showing line items. There's no React code for any of it. The UI Engine renders everything by interpreting the schema.
When a Rakton developer needs to add a new field — say, a 'priority' column on Orders — they edit the schema. The change propagates everywhere: database migration runs, API responses include the new field, the table shows the column, the detail page adds the row, the filter sidebar offers the new dimension. What used to be a 2-hour task across 6 files is now a 5-line YAML diff.
This isn't free. Schema-driven UI gives up something every component library has: pixel-level control over individual views. If a designer wants the Customer detail page to have a hero card with the customer's photo, a stats grid, and a tabbed activity feed — that's not something the generic schema renderer can express. So we layered the system:
Most admin work lives at Level 1. The expensive work — Rakton's dashboard, POS terminal screens, customer-facing storefronts — stays at Level 3 where it should be.
The UI Engine ships a widget library: text inputs, dropdowns, date pickers, money inputs, relation pickers, file uploads, rich text editors. Each widget knows how to render itself for a given field type and how to validate against the schema's rules. Adding a new widget — say, a map picker for a geo:point field — extends the engine for every product at once.
export const MoneyWidget: SchemaWidget<MoneyField> = {
type: "money",
render({ field, value, onChange, errors }) {
return (
<div className="money-input">
<CurrencyPrefix currency={field.currency} />
<NumericInput
value={value}
onChange={onChange}
min={field.min}
max={field.max}
/>
{errors.map((e) => <ErrorText key={e.code}>{e.message}</ErrorText>)}
</div>
);
},
validate(field, value) {
if (field.min != null && value < field.min) return ["below_min"];
if (field.max != null && value > field.max) return ["above_max"];
return [];
},
};Because the UI Engine reads schemas alongside the user's role, permission filtering happens for free. A field marked confidential disappears from the table for users without read access. A field marked readonly renders as a static value in forms instead of an editable input. There's no separate permission check in the UI code because the schema already encodes the rules.
Schema-driven UI is a leverage tool, not a worldview. It pays off when you have many similar CRUD-shaped surfaces that share structural patterns. It does not pay off when:
We built the UI Engine because we run three product lines on the same platform. The leverage is real. For a single-product startup, the same approach would be over-engineering.
Two years in, the Rakton admin has grown to 60-plus entities. The team didn't grow. The component count didn't grow either — most of the codebase is schema declarations, not React. What was a downstream bottleneck became a structural advantage. That's what 'platform first' actually means: when you build the right foundation, the products on top get easier over time, not harder.
Recent articles on platform, security, and engineering.
OAuth solved authorization for a federated web. It does not solve identity for systems where every device, every session, and every request needs to be verifiable. Here's what we built instead.
Read moreWe turned on auto-ban in production six months ago. Here's what the data shows about real-world attacks, false positives, and the thresholds that actually work.
Read moreBearer tokens are a liability. How ECDSA-based proof of possession makes stolen tokens worthless and why every API should require it.
Read more