ObjectStackObjectStack

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

FilePurpose
packages/platform-objects/src/apps/setup.app.tsThe App definition — the navigation shell (group anchors), branding, required permissions
packages/platform-objects/src/apps/setup-nav.contributions.tsSETUP_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 by plugin-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.

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 into group_diagnostics.
  • OAuth Applications (sys_oauth_application) — third-party OAuth client registrations, contributed into group_advanced.
  • Signing Keys (JWKS) (sys_jwks) — not in the nav (#7544). The object is the environment's JWT signing-key store (private_key) and declares enable.apiEnabled: false / apiMethods: [], so the generic data API answers OBJECT_API_DISABLED (404) on every list — for every persona, platform admin included, since that gate reads only the object's enable block and takes no user or permissions. The entry that used to be here carried a manage_platform_settings gate, which could not prune it: a permission gate and an API-disabled object are independent conditions. (sys_verification and sys_device_code are likewise deliberately absent: sensitive, ephemeral secrets — not browsable, and also private. All three are pinned in setup-nav-dead-key-tombstone.test.ts.)
  • Audit Logs (sys_audit_log) — contributed by plugin-audit into group_diagnostics. (The sys_activity and sys_comment objects also live in plugin-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.ts needs 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 App from @objectstack/spec/ui, and contributions against NavigationContribution.

Customising it

You do not edit setup.app.ts to add entries — it only holds the empty group anchors. To add a new entry:

  1. Register the object/dashboard in the package that owns it (or in a project-local plugin if it is project-specific).
  2. Add a NavigationContribution targeting the relevant group id (app: 'setup', group: 'group_*') — either in SETUP_NAV_CONTRIBUTIONS for @objectstack/platform-objects-owned objects, or in the navigationContributions of the plugin that owns the object.
  3. If the entry needs its own permission, declare it where the relevant permissions live (e.g. setup.access is granted in plugin-security's default permission sets) so the requiredPermissions check picks it up.

For project-local Setup-style entries, define a separate App in your project — Setup is reserved for platform-level administration.

On this page