# Modules

> Per-vertical module visibility: what the Modules screen toggles, where the denylist is stored, and how to read or write it over the API.

The **Modules** admin screen controls which parts of a workspace's chrome are
surfaced in a given vertical. It writes a denylist of navigation keys onto the
organization record. Nothing on this screen provisions, buys, or revokes a
capability.

## What module visibility is, and what it is not

Every module in the registry is already available to the organization. The
Modules screen exists so an operator can hide the ones a particular workspace
does not need, and it says so in the screen footer:

> Hidden modules are removed from the robotics chrome for everyone in this org.
> This is a visibility override only — it doesn't change billing or entitlements.

Two consequences follow from that:

- The setting is **org-wide**, not per-user. Hiding a module removes it from the
  vertical's chrome for everyone in the organization.
- The setting is **cosmetic with respect to access control**. It changes what the
  chrome renders, not what the tenant is entitled to.

## Interaction with entitlements and billing

Entitlements and subscriptions gate *access*. Module prefs gate *surfacing*. They
are independent mechanisms stored in different places, and the Modules screen
does not touch the former. A module can be:

- subscribed and shown — the default,
- subscribed and hidden — the case this screen is built for, and
- not relevant to this screen at all if access itself is what you need to change.

Because of that independence, hiding the `voice` or `games` key is still useful
for an organization that *is* subscribed to those verticals: it simply keeps the
tab out of the way while an operator is working inside robotics.

## Where it is stored: module_prefs.&lt;vertical&gt;.hidden

The persisted value is a **denylist of nav keys**, stored per vertical on the
organization:

```
organization.module_prefs.robotics.hidden = ["voice", "games", "billing"]
```

Keys absent from the array are visible. There is no "shown" array — the visible
set is always *registry minus hidden*. The screen's header counter reflects that:
it shows `visible / total`, computed as the registry length minus the size of the
hidden set.

## Registry of nav keys per vertical

The registry is defined in the screen, and only keys in it can be toggled. For
the robotics vertical:

| Key             | Label                 | Group           |
| --------------- | --------------------- | --------------- |
| `voice`         | Voice tab             | Cross-vertical  |
| `games`         | Games tab             | Cross-vertical  |
| `fleet`         | Fleet directory       | Fleet           |
| `robot`         | Robot detail          | Fleet           |
| `topics`        | Topic explorer        | Fleet           |
| `telemetry`     | Connection telemetry  | Transport       |
| `mqtt-clients`  | MQTT · Clients        | MQTT            |
| `mqtt-topics`   | MQTT · Topics         | MQTT            |
| `mqtt-bridge`   | MQTT · Topic policies | MQTT            |
| `layout`        | Layout presets        | Operations      |
| `audit`         | Audit log             | Operations      |
| `alerts`        | Alerts                | Operations      |
| `billing`       | Billing               | Operations      |

The groups — `Cross-vertical`, `Fleet`, `Transport`, `MQTT`, `Operations` — are
presentation only. They order the rows on screen and are not part of the stored
value.

## Cross-vertical keys vs sub-nav keys

The registry mixes two kinds of key, and they act on different pieces of chrome.

**Cross-vertical keys** (`voice`, `games`) correspond to the other verticals in
the top-bar app switcher. Hiding one removes that tab from the switcher while the
operator is on the robotics host.

**Intra-vertical keys** (everything else) correspond to entries in the robotics
sub-nav, as declared by `SUITE_NAV.robotics`. `SuiteShell` filters hidden keys out
of the sub-nav list when it renders, so a hidden key simply does not appear.

## Screens that can never be hidden

The `modules` key itself is not in the registry and is always rendered. That is
deliberate: if the Modules screen could be hidden, a hidden module could never be
un-hidden from the console again.

## Enabling and disabling a module

Each row is a checkbox whose **checked** state means *visible*. Unchecking a row
adds its key to the pending hidden set and stamps the row with a `hidden` chip.

The screen is a staged editor, not a per-toggle writer:

1. The screen loads the saved denylist for the vertical and seeds local state
   from it.
2. Toggling rows or applying a preset changes local state only.
3. The **Save** button is disabled until local state differs from the saved set —
   the comparison is by set membership, so toggling a key off and back on again
   leaves the button disabled.
4. Save writes the whole hidden array in one mutation.

On success the screen shows `Module visibility saved.` On failure it shows the
error message returned by the mutation, or `Save failed.` if none was supplied.

## Presets and custom configurations

Three presets are offered. Each is authored as the set of modules that should be
**shown**; the stored denylist is everything else in the registry.

| Preset | Shown | Resulting hidden keys |
| ------ | ----- | --------------------- |
| `Full robotics` | all | none |
| `Robotics only (hide Voice + Games tabs)` | every key except `voice` and `games` | `voice`, `games` |
| `Transport-only view` | `fleet`, `robot`, `topics`, `telemetry`, `audit` | every other registry key |

Clicking a preset replaces the pending selection outright. A preset button
renders as active only while the current pending hidden set is exactly equal to
the set that preset would produce. After a manual tweak, no preset may match —
the configuration is then simply custom, and that is a valid state to save.

## When a change takes effect

The change takes effect when the mutation succeeds; there is no separate publish
step and no reload prompt. After a successful save the screen invalidates its
cached copy of the prefs query, so the refetched denylist becomes the new
baseline and the Save button returns to its disabled state.

Chrome filtering happens at render time from the loaded prefs, so a session that
is already open elsewhere picks up the new set the next time it reads the prefs.

## Reading and writing prefs over the API

The screen is backed by two procedures, both org-scoped. Use them to read or
write the denylist outside the console.

**Read** the prefs for an organization:

```ts
const prefs = await trpc.adminModules.get.query({ orgId });
// { robotics?: { hidden?: string[] }, ... }
const hiddenInRobotics = prefs.robotics?.hidden ?? [];
```

The response is keyed by vertical. A vertical with no override may be absent, and
so may its `hidden` array; treat both as an empty denylist.

**Write** the full denylist for one vertical:

```ts
await trpc.adminModules.setHidden.mutate({
  orgId,
  vertical: 'robotics',
  hidden: ['voice', 'games', 'billing'],
});
```

`setHidden` is a whole-array write, not a patch. To hide one more module, read the
current array, append the key, and send the result back — sending only the new key
clears every other hidden key for that vertical. Sending `hidden: []` restores the
full registry, which is what the `Full robotics` preset does.

`setHidden` takes the vertical as an argument, so the same procedure backs the
Modules screen in every vertical; a write for one vertical leaves the others'
denylists untouched.

## The Modules screen in other suites

The contact-centre suite ships its own host for this screen. That host supplies
only the admin chrome — the shell, the stack icon, and the `Modules` page header —
and delegates the entire body to the shared `screens/modules` component in the
agent UI package, so both consoles render a single implementation of the body.
When you are looking for the behaviour of the screen body, read the shared
component rather than the host file.

## Related

- [Authentication](/concepts/authentication) — the API key your backend presents
  when calling control-plane procedures such as `adminModules.setHidden`
- [Telemetry](/platform/telemetry) — the data behind the `telemetry` module
