Pages & Templates
Every operator-facing page in the React shell is a data row in the FrontEnd DB. The page itself is not source code — it's a template (an ordered list of component slots) bound to per-slot configuration JSON. Adding, removing, or rearranging pages is a database operation, not a release.
This page covers:
- The three-layer data model (Component → Template → Page).
- The URL structure the React shell uses.
- The four workflows for editing the data model.
- The full set of
MIB3UX_PAGE_CONFIGURATIONSkeys.
Related docs.
Menus — pointing menu entries at pages.
Component Authoring — writing the C# + React halves of a custom widget that slots into a template.
System Overview — RenderV2 — what happens between the React shell's render request and the JSON response.
Conceptual model
Component ┐
ComponentKey │ Reusable widget, identified by a string
AssemblyName │ [ComponentType("...")] on its C# class.
ClassName │
┘
Template ┐
TemplateKey │ Reusable page layout: an ordered list
HasAnchorMenu │ of components.
List<Component> │
┘
Page ┐
PageKey │ A concrete page — one specific template
Template │ bound to per-page settings + per-slot
PageSettings │ component configuration.
PerSlotConfiguration │
┘
Three distinct identifier strings travel through this model — keep them straight:
| Identifier | Scope | Example |
|---|---|---|
ComponentKey |
Identifies the widget class (one per registered C# type). | mib_default_listcomponent, my_custom_widget |
TemplateComponentKey |
Identifies one slot in one template. Unique within a template. | articles_template_001 |
PageKey |
Identifies a page in URLs and menus. | articles_edit, articles_list |
Backing tables in the FrontEnd DB:
| Table | Holds |
|---|---|
MIB3UX_COMPONENTS |
One row per registered widget class (ComponentKey). |
MIB3UX_TEMPLATES |
One row per template (TemplateKey). |
MIB3UX_TEMPLATE_COMPONENTS |
Join rows: which widget instances belong to which templates, in what Order. |
MIB3UX_PAGES |
One row per page, binding a PageKey to one TemplateKey. |
MIB3UX_PAGE_CONFIGURATIONS |
Page-level settings as (PAGE_KEY, KEY) → VALUE rows. |
MIB3UX_PAGE_COMPONENT_CONFIGURATIONS |
Per-page-per-slot widget configuration as (PAGE_KEY, TEMPLATE_COMPONENT_KEY, KEY) → VALUE rows. |
URL structure
The React shell serves all pages from a single SPA host. URLs are:
| Pattern | Mode | Example |
|---|---|---|
~/app/<pageKey> |
List mode (no ids) | ~/app/articles_list |
~/app/<pageKey>/<id> |
Edit mode (single record) | ~/app/articles_edit/12345 |
~/app/<pageKey>/<id1>,<id2>,… |
Bulk-edit mode (multiple records) | ~/app/articles_edit/12345,12346,12350 |
The shell parses the URL, issues
GET /api/v2/display/<pageKey>/<id> against the FrontEnd Server,
receives {schema, data}, and dispatches each component
schema.components[i] to either a built-in widget or a federated
remote — see Overview for the full flow.
Editing the data model
Four workflows in roughly increasing order of formality:
1. Development Tool UI
An admin UI inside the FrontEnd Server's local-development host lets you create / edit pages, drag widgets into templates, and edit configuration JSON for each slot interactively. Best for prototyping and for one-off changes during development.
See Local Development. Production environments typically don't expose the dev tool UI.
2. SQL migrations (production path)
For production, ship the rows as SQL migrations against the FrontEnd
DB through mibmigrator. A typical dev cycle:
- Use the dev tool to lay out the page interactively.
- Export the resulting INSERT/UPDATE statements (or hand-write a migration mirroring the rows).
- Commit the migration into the customer's customisation repository
(
<Customer>.Mib3.MibDatabaseMigrationsor equivalent). - Apply the migration in the target environment with
mibmigrator.
See Framework — Migrations for the migration tooling.
3. Customer customisation seed code
A customer's C# customisation repo typically wraps the migration
boilerplate in a Seed helper:
seed.AddOrUpdatePage("articles_edit", "articles_template");
seed.AddOrUpdateTemplateComponent(new UxTemplateComponent
{
ComponentKey = "mib_default_formcomponent",
TemplateComponentKey = "articles_template_main_form",
Order = 10,
}, "articles_template");
seed.AddOrUpdatePageComponentConfiguration(
"mib_default_formcomponent",
"articles_template_main_form",
new Dictionary<ComponentConfigurationKey, string>
{
{ ComponentConfigurationKey.MediaType, "ARTICLES" },
{ ComponentConfigurationKey.Title, "Article" },
{ ComponentConfigurationKey.Readonly, "false" },
},
"articles_edit");
Browse the customer customisation's src/Database/Migrations/ for
examples of every operation (add, update, remove, reorder).
4. Direct SQL (read-only or emergency only)
For investigation, query the tables directly:
-- All pages
SELECT PAGE_KEY, TEMPLATE_KEY FROM MIB3UX_PAGES;
-- All slots on one page's template
SELECT TC.TEMPLATE_COMPONENT_KEY, C.COMPONENT_KEY, TC.[ORDER]
FROM MIB3UX_TEMPLATE_COMPONENTS TC
JOIN MIB3UX_COMPONENTS C ON TC.COMPONENT_KEY = C.COMPONENT_KEY
JOIN MIB3UX_PAGES P ON P.TEMPLATE_KEY = TC.TEMPLATE_KEY
WHERE P.PAGE_KEY = 'articles_edit'
ORDER BY TC.[ORDER];
-- Per-slot configuration on one page
SELECT TEMPLATE_COMPONENT_KEY, [KEY], VALUE
FROM MIB3UX_PAGE_COMPONENT_CONFIGURATIONS
WHERE PAGE_KEY = 'articles_edit'
ORDER BY TEMPLATE_COMPONENT_KEY, [KEY];
-- Page-level settings
SELECT [KEY], VALUE
FROM MIB3UX_PAGE_CONFIGURATIONS
WHERE PAGE_KEY = 'articles_edit';
Direct UPDATEs against these tables work but never do this in production — the rows must be expressed as migrations so other environments stay in sync.
Page-level settings — MIB3UX_PAGE_CONFIGURATIONS
Each row in this table is a single (PAGE_KEY, KEY) → VALUE setting.
A page can have many entries, one per setting. Boolean values may be
stored as "true"/"false" strings; integers and other primitives
are stringified accordingly.
ALLOW_AUDIT_VIEW
Enables the audit panel for the page. true / false.
When true, a button in the top-right opens the audit drawer
(Audit Trail). The button is only useful when
the EditHistory Microservice is
configured (UseEditHistoryMicroService=true on the BFF) — otherwise
the drawer opens empty.
RETURN_URL
The URL the page's "Return" button navigates to. Used to override the default of "the previous page in browser history".
Important
Mandatory for edit pages: when a user lands on an edit page
directly (e.g. via a deep link from email or a bookmark), the
browser has no history to "go back" to. Without RETURN_URL the
Return button breaks.
Example:
RETURN_URL = ~/app/articles_list
RETURN_AT_SAVE
true / false. When true, the page navigates to RETURN_URL
(or the previous page) immediately after a successful save, rather
than refreshing the edit page with the saved record. Useful for
"create then return to list" flows.
RESTORE_STATE
true / false. When true, the shell restores the page's
client-side state (scroll position, list filters, pagination, sort,
selection) when the user navigates away and comes back.
Implementation: state is keyed by PageKey in sessionStorage.
Query-string parameters are not part of the saved state — they
have to be reproduced from the page's own configuration.
DISABLE_CLONE_WITH_RELATED
true / false. Default false. When true, the Clone with
related option in the page's action menu is hidden; only
Clone (without related) remains. Use when the cost of duplicating
child records is too high to be a safe single click.
ALERT_DELETE_MESSAGE
Custom text shown in the delete-confirmation modal. Accepts plain
text or a {DICT:Section/Key} translation marker (see
Localization).
ALERT_DELETE_MESSAGE = {DICT:Articles/DeleteConfirmText}
If unset, the shell falls back to a default message.
CUSTOM_CONFIGURATION
A JSON blob of free-form properties merged into the page-render
response and exposed to every component on the page. The shell
puts the parsed object on response.configuration — custom widgets
read it via the React useContentContext() hook or by reading the
prop their parent passes down.
Use for:
- Customer-wide feature flags scoped to a specific page.
- Per-page UX tweaks that don't fit any standard key.
- One-off values consumed by a custom widget.
{
"showExperimentalView": true,
"maxBulkEditRows": 50,
"defaultLanguage": "en-us",
"feature": { "enablePreview": false }
}
Stored as a JSON string; parsed on every render. Stick to flat structures where possible — nested objects work but make debugging harder.
SORTER
Boolean. When true, the BFF includes a sorter flag in the render
response and the shell shows the tab-sorter UI anchored to the
gear icon in the top-right of an edit page. Each tab row in the
sorter has a drag handle on the left and a visibility toggle (eye)
on the right — drag to reorder, click the eye to hide a tab from
the user's view:

Pairs with the useSorter() shared hook
— custom widgets that decorate a sorted layout can read the current
sort order from the shell.
Per-slot configuration — MIB3UX_PAGE_COMPONENT_CONFIGURATIONS
Each row binds one configuration key on one slot on one page to a value:
(PAGE_KEY, TEMPLATE_COMPONENT_KEY, KEY) → VALUE
The available keys vary by component type — see the per-component docs:
- List Component — list-style data
- Form Component — single-record form
- Simple Related List — related records as list
- Ordered Related List — same with drag-reorder
- Related Form — related records as forms
- Related Auto Complete — autocomplete picker
- Content Criteria — content selection expression
- DMM — DMM job status
- Message — static text
- iFrame — embedded external page
Some keys are shared across most components (e.g. TITLE,
MEDIATYPE, READONLY, ALLOW_BULK_EDIT); each component doc
lists its full set.
Inspecting a configured page
Three practical ways to see how a page is set up:
A. Network panel
Open the page in the React shell, open DevTools → Network, and
inspect the GET /api/v2/display/<pageKey>/<id> response. The body
is one JSON object with the whole rendered page:
{
"pageKey": "articles_edit",
"title": "Articles",
"configuration": { /* page-level settings, merged with CUSTOM_CONFIGURATION */ },
"schema": { "components": [ /* one per slot, schema only */ ] },
"data": { "components": [ /* one per slot, values only */ ] }
}
This is the canonical "what does the operator actually see" — what the shell receives is what the shell renders.
B. Direct SQL
SELECT [KEY], VALUE
FROM MIB3UX_PAGE_CONFIGURATIONS
WHERE PAGE_KEY = 'articles_edit';
SELECT TEMPLATE_COMPONENT_KEY, [KEY], VALUE
FROM MIB3UX_PAGE_COMPONENT_CONFIGURATIONS
WHERE PAGE_KEY = 'articles_edit'
ORDER BY TEMPLATE_COMPONENT_KEY, [KEY];
C. Development Tool
The dev tool shows page + slot configuration in a structured editor — useful for non-developers. See Local Development.
Common gotchas
RETURN_URLunset on an edit page — Return button breaks for deep-link arrivals. Always set it.SORTERenabled but no operator-facing benefit — tab sorter only helps when an edit page has many sibling components. Don't enable it on simple form-only pages.CUSTOM_CONFIGURATIONblob too deep — nested objects three levels down become invisible during debugging. Flatten where you can.- A new page key but no menu entry — the page works at its URL
but operators can't navigate to it. Add a row to
MIB3UX_MENU— see Menus.
See also
- Menus — pointing menu entries at pages
- Component Authoring — how to author a widget that slots into a page template
- Local Development — the dev-tool UI for editing page templates and configurations
- System Overview — RenderV2 — what happens between the React shell's render request and the JSON response
- React Hooks & Events — including the
useSorter()hook gated bySORTER - Audit Trail — pairs with
ALLOW_AUDIT_VIEW - Localization —
{DICT:...}markers in configuration strings - Built-in Components — List, Form, Related List, …