Web admin
The host serves a web admin (React single-page app, built from ui/) at the root of the same host and port,
e.g. http://localhost:5001/ for the first organization of xrmd server init (http://localhost:5000/ with the repository
compose file). The Docker image builds it into wwwroot (docker/Dockerfile); outside Docker the host
serves the directory in Ui:RootPath (default wwwroot under the content root) and, if it holds no build, a placeholder
page. The shell uses only the Web API and the Admin API: every write goes through the Core, so the pipeline, plug-ins and
security apply as for any other client. The anonymous GET /ui/config.json publishes the public values the shell boots
from (authMode, audience, authority, clientId, scope, apiVersion); never the signing key.
Who can use it. Users holding the System Administrator or System Customizer role (directly or through an
owner team; recognised by the role template, not by name). Anyone else sees an "Access denied" page naming the two roles.
The Environment section (health, doctor, version, and the forms' auto-save switch, organization.isautosaveenabled) and the cascade solution delete use the Admin API and are
shown only to System Administrators; a Customizer who opens /manage/environment sees "Requires System Administrator".
Sections (all under /manage/*, because /admin/* is the Admin API): Environment, Users, Teams, Security roles,
Business units, Tables (columns, relationships, records), Solutions (import, export, delete), Plug-ins (assemblies,
steps, trace log), Async operations, Audit, Duplicate detection, Field security and Hierarchy security (System Administrators only) and About (the trademark notice, where the third-party notices are, and a link to
SECURITY.md; every signed-in admin or customizer sees it). /apps and /main.aspx open the model-driven apps of
imported solutions, for any signed-in user: see Model-driven apps. Server paths (/api, /admin, /mcp, /XRMServices,
/metrics, /healthz, /ui/config.json, /WebResources, /Image) are never answered with the shell.
Look and feel
The web admin, the sign-in page and the model-driven apps share one theme: the xrmverse Indigo palette
(ui/src/shell/theme.ts) on Fluent UI v9, our own logo and name in a dark header bar, and the system font stack
(the brand font Sora is not shipped yet, #11). Each admin page has a title on its own line with its main action on the
right; pages below a list (a table, a record) show a breadcrumb of the parent pages above the title. The section
navigation highlights the current section; on narrow windows (600 px and less) it collapses behind a Menu button in
the header and closes when a section is chosen.
Signing in
- Development (
AUTH_MODE=Development). The sign-in page asks for the signing key (AUTH_DEV_SIGNING_KEY), an object id (default: the seed administrator00000000-0000-0000-0000-000000000001) and a display name. The browser mints the same HS256 token asdocker/dev-token.shwith WebCrypto; no server endpoint mints tokens. The key is kept insessionStorageonly (gone when the tab closes), never inlocalStorage. WebCrypto needs a secure context: open the shell over HTTPS or onlocalhost; elsewhere the page says WebCrypto is unavailable. - Microsoft Entra ID (
AUTH_MODE=Entra). The shell signs in with MSAL.js (redirect flow) and needs its own SPA app registration, separate from the API's:- In the tenant, register an application with a Single-page application platform and the redirect URI of the
shell: the origin of the page plus
/(e.g.http://localhost:5000/, or your HTTPS URL). - Under API permissions add the scope exposed by the API registration (the one whose Application ID URI is
AUTH_AUDIENCE). - Configure the server with the SPA's client id and that scope:
Ui:Entra:ClientIdandUi:Entra:Scope(UI_ENTRA_CLIENT_IDandUI_ENTRA_SCOPEin.env, which the compose files map toUi__Entra__ClientIdandUi__Entra__Scope; empty by default). The authority is derived fromAuth:TenantId(https://login.microsoftonline.com/{tenant}/v2.0). If any of the three is missing, the shell shows a configuration error naming the keys instead of a sign-in button.
- In the tenant, register an application with a Single-page application platform and the redirect URI of the
shell: the origin of the page plus
Errors from the server are shown with the Dataverse error code and message. Lists page 50 records at a time
(Prefer: odata.maxpagesize=50, @odata.nextLink).
Audit
The Audit section (/manage/audit, see Auditing):
- switches auditing of the organization on or off and sets the retention in days or forever (System Administrator only; others see the settings read-only);
- lists the audit log, newest first, filtered by table, operation and user, and opens an entry in a drawer with the old
and new value of each column. Reading it needs
prvReadAuditSummary; without it the page shows the server's 403.
In Tables, a table has an "Audit this table" switch and each column an "Audit" switch; a setting the server has
fixed is disabled. A record form of an audited table shows its Audit history, read from audits and
RetrieveAuditDetails (it needs prvReadAuditSummary). The web admin does not delete audit data: use xrmd audit delete or the messages.
Field security
The Field security section (/manage/security/fieldprofiles, see Field-level security) is
shown only to System Administrators, the only role that holds the profile privileges. It lists the profiles, creates and
deletes them, and edits a profile's permissions in a matrix of secured columns (display label, logical name below)
against read, create and update; each toggle writes the fieldpermission row. It gives a profile to users and teams and
takes it back. The System Administrator profile is shown read-only.
In Tables, each column has a Secured switch, disabled when the column cannot be secured. A record form marks secured columns with a lock and shows "No access" for a secured column the server left out of the record.
Hierarchy security
The Hierarchy security section (/manage/security/hierarchy, see Hierarchy security)
is shown only to System Administrators. It switches the manager hierarchy model on or off, sets how many levels of reports a
manager reaches (1 to 100) and lists the excluded tables, which you add by logical name and include again one by one. Saving
writes the settings through the Admin API; the server judges every value and its refusal is shown as it is.
Access team templates
The Access team templates section (/manage/security/accesstemplates, see Access teams) lists
the templates with their table, name and rights, one checkbox per right (Read, Write, Append, AppendTo, Delete, Share, Assign).
It creates a template for a table, changes its rights (each click is one update, and only teams created afterwards get the
new rights) and deletes it. Delete asks for confirmation first, because the access teams made from the template are deleted
too. A Auto-create access teams switch for the chosen table is on the page (a template can be created only for a table
that is switched on) and on the table's own page in Tables. The page is not limited to System Administrators in the
menu, but the server answers with its 403 (shown as it is) to a user without the teamtemplate privileges, which in the
standard roles only System Administrators hold. The web admin does not add users to a record's team: use
AddUserToRecordTeam (SDK, Web API) or xrmd access-team add.
Change tracking
On a table's page in Tables, a Track changes switch turns change tracking on for the table (see
Change tracking). Change tracking cannot be turned off again, so the switch asks for confirmation
first and, once on, stays on and disabled with a hint. It is disabled with a hint too when the table cannot be tracked (an
intersect table, audit and the other tables the page lists as ineligible); the server's refusal of any other case is shown
as it is. The retention (expirechangetrackingindays) is not on a page: use xrmd change-tracking retention or the
organization row.
Duplicate detection
The Duplicate detection page (/manage/duplicates, open to System Administrators and System Customizers) manages duplicate
detection (see Duplicate detection). At the top, four switches set the organization's columns:
duplicate detection, detect when a row is created or updated, detect on import and detect on offline sync; the last two carry a
hint that they are stored and nothing applies them. Below, the table lists the rules with their base table, matching table and
status (Unpublished, Publishing or Published). New rule and Edit open a dialog with the name, description, base and
matching table (only tables with duplicate detection on are offered; the tables cannot change on an existing rule), the two
checkboxes (compare text with case, ignore inactive rows of the matching table) and the conditions: a base column, a matching
column, an operator (exact match, same first or last characters with a count, same date, same date and time, exact match of
option value) and ignore blank values. Publish starts the system job and the page shows its progress (waiting, running,
ended) until the rule is Published or the job's message explains the failure; Unpublish and Delete ask for
confirmation first. A published rule cannot be edited: unpublish it first.
On a table's page in Tables, a Duplicate detection switch sets IsDuplicateDetectionEnabled (turning it off unpublishes
the rules of the table, as the hint says). The server judges every value and its message is shown as it is. A rule is saved in
several writes, so when one is refused the dialog shows the server's text and a note that the rule may be partly saved; it then
lists the conditions the server holds, and you fix the problem and save again. Running RetrieveDuplicates and
BulkDetectDuplicates is not on a page: use xrmd duplicate-detection find and bulk, or the Web API.
Activities and notes
In Tables (see Activities and notes):
- New table has an Activity table switch. When it is on, the primary name column is
Subject(shown and disabled) and the ownership is user (disabled); the table is created as a custom activity. This cannot change later. - A table's page has a Notes switch and an Activities switch (
HasNotes,HasActivities). Each asks for confirmation before it saves, because neither can be turned off again; once on, the switch stays on and disabled with a note. An activity table shows "This is an activity table" and no Activities switch. When the server does not report a setting, no switch is shown; a refusal of the server is shown as it is. - A record form of an activity shows its party lists (
from,to,requiredattendees, …) read-only, read through$expand={activity}_activity_parties: each participant as "Name (table)", with the table's logical name; a participant with no name as the table and id; an address alone as the address; "None" for an empty list. Party lists are never sent on create or update, and are not offered as list columns. Change participants with the SDK or the Web API. - A note's form hides
documentbody. Timelines, activity forms and a note editor are not part of the web admin.
Currencies
The Currencies page (/manage/currencies, System Administrators only) lists the currencies with code, name, symbol, precision, exchange rate and
state; the base currency has a Base badge. New currency opens a dialog for a System currency (ISO 4217 code) or a
Custom one (three capital letters), with name, symbol, precision and rate. A rate is edited in the row and saved with Save
(the base currency's rate is fixed at 1, with a hint). Deactivate asks for confirmation; Activate does not; both are
disabled for the base currency. Pricing precision sets organization.pricingdecimalprecision (0 to 4). The page says that
changing a rate does not change existing records until a save changes their money or currency. A column with no value (a
NULL rate, for example) is shown empty. A refusal from the server is shown in a message bar as it is, inside the dialog for a
new currency (the dialog stays open). In a
record form the currency is a normal lookup; exchangerate and the _base columns are not offered, because the server sets
them. See Currencies.
Deleting a solution in the UI
The Solutions page offers the same two options as the CLI (see Deleting a solution):
- Delete solution removes only the solution container, as in Dataverse. Its components stay in the system, tracked by the Default solution.
- Delete solution and its components is xrmverse-only (System Administrator only): it also drops the components (tables and their data) that no other solution holds. It goes through the Admin API, requires typing the solution's unique name, and cannot be undone.
Known limitations
Anything the server cannot do faithfully is reported, never emulated:
- Business unit disable. The UI sends
isdisabledlike Dataverse, but the server'sbusinessunittable has noisdisabledcolumn yet, so the server answers with an explicit error, which the UI shows. - Cascade delete preview. The confirmation dialog lists the solution's components; which of them are kept (system components, components held by another solution) is only known after the delete, because the server has no dry run. The result then lists deleted and kept components with the reason.
- Plug-in assemblies are registered with the CLI (
xrmd plugin register <dll> [--solution <name>]): the CLI inspects the DLL on your machine. The Plug-ins page browses assemblies, types and steps, and enables or disables steps; it lists plug-in steps only, so a webhook and its steps are managed withxrmd webhook, the SDK or the Web API (see Webhooks). Its Custom APIs tab lists the custom APIs read-only (binding, kind, private, plug-in type, parameters and properties); it does not create or change them: usexrmd customapi create, the SDK or the Web API, or import a solution (see Custom APIs). - New schema names. When you create a table or column the UI suggests
<default publisher prefix>_<PascalCase display name>(for examplexv_ProjectTask); you can edit it. The logical name is the lowercased schema name. If the default publisher has no prefix, no name is suggested. - Server gaps the UI works around.
EntityDefinitionsdoes not expand relationships, so the UI readsRelationshipDefinitionsto find the navigation property of a lookup. A polymorphic lookup accepts one navigation name per attribute on the server; the UI binds with that name and never guesses one.
Development loop
pnpm -C ui install
pnpm -C ui dev # http://localhost:5173, proxies /api, /admin and /ui/config.json to http://localhost:5000
pnpm -C ui test # unit tests (Vitest)
pnpm -C ui typecheck
pnpm -C ui build # writes ui/dist
pnpm -C ui e2e # Playwright scenarios against a real host and SQL Server
Run the .NET host on port 5000 first (dotnet run --project src/Xrmd.Host with the configuration of
docker/.env, or the Docker stack). The host is built without the Microsoft SDK and exits with code 3 until it finds it:
install it once with xrmd sdk install (into ~/.xrmd/sdk), or, as a developer, point XRMD_SDK_DIR at the restored
package, ~/.nuget/packages/microsoft.powerplatform.dataverse.client/1.2.27/lib/net10.0 (NUGET_PACKAGES if you set it).
pnpm -C ui e2e sets XRMD_SDK_DIR to that folder itself and stops with a message when it is missing (run
dotnet restore xrmverse.sln). pnpm -C ui e2e starts its own SQL Server with docker run, builds the UI, the host
and the fixture solution, and runs the host in development mode on a free port; it needs Docker, the .NET SDK, zip
and a Chromium (XRMD_E2E_CHROMIUM, else /opt/pw-browsers/chromium, else Playwright's own). See ui/README.md.
The manual acceptance checklist is docs/superpowers/acceptance/web-admin-checklist.md.