Setup app — administer a running runtime
The platform's built-in administration UI — what it is, what it shows, and how it is wired
The Setup App (/apps/setup) is the built-in administration console
for every ObjectStack project. It surfaces the platform sys_* objects,
the System Overview dashboard, and a set of settings pages behind a
left-hand navigation tree.
Under ADR-0029 D7 the app is not a fixed navigation tree. SETUP_APP
ships as a thin shell of stable, empty navigation group anchors
("slots"); the actual menu entries are contributed at runtime by the
packages that own the underlying objects, via navigationContributions.
The runtime merges every contribution into the app's navigation tree by
group id + priority on read, so the rendered menu reflects exactly which
capability plugins are loaded — a disabled capability contributes nothing
and its slot stays empty.
The app itself ships from the dedicated @objectstack/setup package
(package id com.objectstack.setup), which registers it at runtime. Per
ADR-0048 (one app per package), /apps/setup — resolvable as
/apps/<packageId> — maps to exactly this app.
Where it lives
| File | Purpose |
|---|---|
packages/platform-objects/src/apps/setup.app.ts | The App definition — the navigation shell (group anchors), branding, required permissions |
packages/platform-objects/src/apps/setup-nav.contributions.ts | SETUP_NAV_CONTRIBUTIONS — the nav entries owned by @objectstack/platform-objects, merged into the shell at runtime |
packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts | "Overview → System Overview" dashboard |
packages/apps/setup/src/index.ts | @objectstack/setup package — SetupAppPlugin.start() performs the runtime registration via manifest.register({ apps: [SETUP_APP], navigationContributions: SETUP_NAV_CONTRIBUTIONS }) |
The
requiredPermissions: ['setup.access']permission declared on the Setup App is contributed byplugin-security(in its default permission sets), not by the package that registers the app. Other capability plugins (e.g.plugin-audit,plugin-webhooks,plugin-approvals) contribute their own Setup nav entries into the shared group anchors.
Navigation
setup.app.ts defines the stable group anchors (the shell); the entries
under each group are merged in from the contributing packages. The group
anchors are:
| Group (anchor id) | Filled by |
|---|---|
Overview (group_overview) | System Overview dashboard — platform-objects |
Apps (group_apps) | Packages — platform-objects; marketplace — cloud-connection (capability plugin) |
People & Organization (group_people_org) | Users · Organization · Business Units · Teams · Organizations · Invitations — platform-objects |
Access Control (group_access_control) | Positions / Permission Sets — plugin-security; Sharing Rules / Record Shares — plugin-sharing; API Keys — platform-objects |
Approvals (group_approvals) | Approvals Inbox (the approvals:inbox component) · Requests · Action History · Delegations (OOO) — plugin-approvals |
Configuration (group_configuration) | All Settings · Localization · Company · Branding · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — platform-objects |
Diagnostics (group_diagnostics) | Sessions · Notification Events — platform-objects; Audit Logs · Audit Log Browser — plugin-audit |
Integrations (group_integrations) | plugin-webhooks |
Advanced (group_advanced) | OAuth Applications · Identity Links · User Preferences — platform-objects |
The exact rendered menu depends on which capability plugins are loaded. A few notable entries:
- Sessions (
sys_session) — better-auth session records, contributed intogroup_diagnostics. - OAuth Applications (
sys_oauth_application) — third-party OAuth client registrations, contributed intogroup_advanced. - Signing Keys (JWKS) (
sys_jwks) — not in the nav (#7544). The object is the environment's JWT signing-key store (private_key) and declaresenable.apiEnabled: false/apiMethods: [], so the generic data API answersOBJECT_API_DISABLED(404) on every list — for every persona, platform admin included, since that gate reads only the object'senableblock and takes no user or permissions. The entry that used to be here carried amanage_platform_settingsgate, which could not prune it: a permission gate and an API-disabled object are independent conditions. (sys_verificationandsys_device_codeare likewise deliberately absent: sensitive, ephemeral secrets — not browsable, and alsoprivate. All three are pinned insetup-nav-dead-key-tombstone.test.ts.) - Audit Logs (
sys_audit_log) — contributed byplugin-auditintogroup_diagnostics. (Thesys_activityandsys_commentobjects also live inplugin-audit, but they are not contributed as Setup nav entries.)
When a contribution names an anchor that is not there
Aiming at a group id the target app does not declare is not refused and
the entry is not dropped: the items are appended at the app's top level
and the merge continues. That is deliberate — the merge is a read-time fold
precisely so registration order does not matter (a contributor may register
before the app it aims at), and a contribution into an optional anchor has to
keep working when the plugin owning that anchor is not loaded.
It is, however, loud. The relocation emits a
nav_contribution_group_missing diagnostic at warn — naming the contributing
package, the target app, the missing group id and the relocated items — so it
survives OS_REGISTRY_LOG=warn and appears in boot output. The same finding is
carried on the app itself, readable as
registry.getAppNavDiagnostics(appName), and it is raised once per distinct
mis-aim rather than once per read of the app.
os build and os validate answer the same question at compile time
whenever the contributing package and the target app are composed into one
artifact. Both print the finding and both carry it in --json under the
existing warnings key, beside the authoring-rule advisories and the
capability hints — the payload is deliberately closed, so a new class of
finding fills a declared key rather than adding one. They report there;
neither fails the build.
⚠️ The anchor id is the whole contract between a shell and its contributors, and a contributor cannot see the shell's ids at authoring time. A typo therefore produces a menu that renders, passes a smoke test, and has silently moved the entry one level up — which is why the diagnostic exists rather than a refusal.
Why a shell + contributions
The Setup App is a shell of empty group anchors rather than a fixed
navigation tree because the objects it surfaces are owned by different
capability plugins (auth / security / sharing / audit / webhooks /
approvals). Letting each package contribute its own menu entries keeps the
menu for an object shipping with the package that owns the object — the
UI-layer analog of object extend. Benefits:
- The rendered menu always matches the loaded capabilities: a disabled plugin contributes nothing and its slot stays empty.
- No code in
setup.app.tsneeds to know about objects that live in other packages; entries are merged by group id + priority on read. - The shell can still be type-checked against
Appfrom@objectstack/spec/ui, and contributions againstNavigationContribution.
Customising it
You do not edit setup.app.ts to add entries — it only holds the empty
group anchors. To add a new entry:
- Register the object/dashboard in the package that owns it (or in a project-local plugin if it is project-specific).
- Add a
NavigationContributiontargeting the relevant group id (app: 'setup',group: 'group_*') — either inSETUP_NAV_CONTRIBUTIONSfor@objectstack/platform-objects-owned objects, or in thenavigationContributionsof the plugin that owns the object. - If the entry needs its own permission, declare it where the relevant
permissions live (e.g.
setup.accessis granted inplugin-security's default permission sets) so therequiredPermissionscheck picks it up.
For project-local Setup-style entries, define a separate App in your project — Setup is reserved for platform-level administration.
Related
- App Metadata —
Appschema reference - Authentication — where
setup.accessis granted - Deployment Overview — the platform-runtime lifecycle that serves Setup; ObjectStack Cloud is one venue inside it
Doc Metadata
Ship package documentation as metadata — flat src/docs/*.md files compiled into the manifest and rendered in the console, ordered by a book navigation spine
Create form ≠ edit form
Derive a short create form and a full sectioned edit form from one flat field set, and hand-shape the create form only when its layout truly differs.