Skip to content

Mount read-only inspector screens

Some bundled features ship read-only inspector screens over their internal read-models: a list (and detail) view an operator can open to see what is going on — GDPR export jobs, invalid download attempts, and so on. The screens live in the feature; your app only opts in by navigating them. No per-app screen code, no custom queries.

The screens are registered but inert — they appear nowhere until your app navs them. Add one r.nav per screen in any of your app features. They are SystemAdmin-gated, so they surface only for platform operators:

defineFeature("my-app", (r) => {
r.requires("user-data-rights");
r.nav({
id: "gdpr-export-jobs",
label: "GDPR · Export Jobs",
// qualified screen name: <feature>:screen:<id>
screen: "user-data-rights:screen:export-job-list",
access: { roles: ["SystemAdmin"] },
order: 90,
});
});

That is the entire wiring. The list, the detail and their query handlers all live in the feature — your app references the screen by its qualified name. The nav resolves cross-feature with no import or coupling, and validateBoot fails loudly if the screen name is wrong.

FeatureScreensShowsGating
user-data-rightsexport-job-list, export-job-detail, download-attempt-listGDPR Art. 20 export requests (user, status, requested / completed, expires) and invalid download attempts (result, IP, attempt time) — DPO brute-force triageSystemAdmin

This table grows as more features expose inspector screens.

Inspector screens use the entityList / entityEdit machinery, which binds to an event-sourced r.entity read-model (rebuild-safe). Features whose read-model is a direct-write store (jobs, sessions, billing-foundation) cannot use this pattern — registering those as entities would wipe the live table on the next projection rebuild. A separate query-bound read-only primitive for those is tracked as a follow-up.