The Settings admin tool of Enonic XP: one page that discovers the
administration sections other applications provide and hosts them side by side. Applications comes
from app-applications; Users, Groups, Roles and ID Providers come from app-users. This app owns
the frame — the app bar, the section rail, the theme, notifications, the url — and renders nothing
of a section itself.
A section is an admin extension on the settings.section interface. How one is discovered, mounted,
routed and revoked is documented in docs/extensions/.
The app is a system app. Its admin tool is open to everyone who can reach XP admin; which sections a visitor sees is decided by each extension's own access rules, server-side.
- JDK 25
- Enonic XP 8.1
Node and pnpm are not required: Gradle downloads the pinned versions (Node 24.18.0, pnpm 10.33.4) into .gradle/ on the first build.
Copy the built JAR to $XP_HOME/deploy, or let Gradle do it:
./gradlew deploy
Nothing shows until a provider is installed: with none, the tool reports that no administration applications are available.
Optional, in $XP_HOME/config/com.enonic.xp.app.settings.cfg:
| Key | Default | Effect |
|---|---|---|
contentSecurityPolicy.enabled |
on | false sends no Content-Security-Policy header at all, for the sections too. Any other value is on. |
contentSecurityPolicy.header |
none | A raw header value unioned onto the policy last, so a directive can be widened without restating the baseline. Invalid tokens are skipped. |
Both values are trimmed, so trailing whitespace in the file is harmless.
The header this tool sends is the whole policy for the page, and a section that needs a remote source adds it itself — so turning the switch off turns off every section's contribution with it. A section's own settings live in the app that provides it.
./gradlew build
This runs the whole pipeline: format, lint, type-check of both frontend and server code, tests, asset bundling, server-side TypeScript compilation, and packaging into build/libs/app-settings.jar.
To skip the checks while iterating:
./gradlew build -x pnpmCheck
Production is the default. For a development build — sourcemaps, no minification:
./gradlew build -Penv=dev
The value must be exactly
dev.-Penv=developmentstill produces an unminified build, but the XP Gradle plugin writes theX-Source-Pathsmanifest header only fordev, so that JAR will not support live reload.
-Penv=dev records the absolute paths of src/main/resources and build/resources/main into the JAR manifest. XP then serves resources from those directories instead of from the JAR, so the deployed JAR can sit anywhere — rebuilding the sources is enough.
Full loop — checks, build and continuous redeploy:
./gradlew dev
Frontend-only loop, much faster, rebuilds assets into build/resources/main on every save:
pnpm dev
pnpm dev covers assets/ only. Files under src/main/resources such as main.html, descriptors and i18n phrases are read straight from source and need no rebuild, while server-side TypeScript needs pnpm pack:server or the Gradle loop.
pnpm check # format, lint, types, tests — what CI runs
pnpm check:fix # same, but fixes formatting in place
pnpm test # tests only
pnpm test:watch
The frontend under src/main/resources/assets/js:
app/ shell, router, the host object handed to each section, section mounting
widgets/ the rail, the mount slot, the empty state, the toast list
entities/extension/ discovery of the sections and their live rediscovery
shared/ api client, config, i18n, admin events, notifications, the mount contract
Tests live next to the code they cover, as *.test.ts.
The server side of src/main/resources:
admin/tools/main/ the single admin tool: descriptor, controller, page template
lib/ tool config, i18n, the admin guard, the CSP baseline, and the hub topics the shell publishes
i18n/ the shell's phrases
The left rail has one entry per discovered section. Hash history is used, and a section is routed as
/{slug}/{sub-path}, where the sub-path belongs to the section itself and the shell only carries it
— so a deep link survives a reload. A path no section answers to goes to the first one.