Menus — overview
A menu in Liberty is what the user sees in the left navigation panel when they open one of your apps. It groups screens, endpoints, dashboards and routes into a tree of folders and leaves.
Three things are worth knowing up front:
| Fact | Implication |
|---|---|
| There is no separate "app" object in Liberty. | An "app" is just a connector that has a menu attached and whose show_in_switcher flag is on. Both conditions are required for the top app switcher to show its tile. |
Menus are stored as a flat list of items linked by parent. | The tree is assembled by the backend — easier to hand-edit, round-trips cleanly through TOML, drag operations only ever mutate one list. |
Every menu key is a connector name ([menus.<connector>]). | The connector named crm carries the menu under [menus.crm]; the screen customers on crm is reached from a leaf with target = "customers_get". |
The page that manages menus is Settings → Menus.
The Menus page at a glance
Three regions:
| Region | What it carries |
|---|---|
| Top scope bar | One chip per app (a connector that has a menu). Click a chip → its tree loads below. The + Add a menu for a connector button registers a new connector under Menus. Discard / Save on the right commit or revert page-wide edits. |
| Tree (left column) | The selected app's menu tree. Click a row to select it for editing. On hover, action icons appear over the row's right edge — Move up / down, Indent / Outdent, Add child, Delete. A filter input narrows the list. |
| Inspector (right column) | The full editor for the selected item — a generic form over the MenuItem schema. Fields adapt to the selected type (a folder shows fewer fields than a leaf). |
What a menu carries
The schema's top-level shape:
| Field | Notes |
|---|---|
label | The app's display name. Falls back to the connector name. Shown in the top app switcher. |
items | A flat list of menu items, linked by their parent field. |
Each item:
| Field | Required | What it does |
|---|---|---|
id | yes | Unique inside this menu. Referenced by children's parent. |
parent | no | Pick a folder, or leave blank for a top-level item. |
label | yes | The sidebar text. |
l | no | Per-language label overrides — l.fr, l.de, etc. |
icon | no | A Lucide icon name (shield, users, chart-bar, …). |
type | no | Blank = folder. Otherwise one of query, endpoint, dashboard, page (see Item types). |
connector | no | Connector hosting the target. Blank = the app's own connector (the menu key). |
target | conditional | Required on every leaf; ignored on folders. |
params | no | Fixed parameters passed to the target when the item opens. |
roles | no | Restrict to these roles. Empty = visible whenever the user can run the target. |
Folder vs leaf
The type field decides:
| Setting | Behaviour |
|---|---|
type blank | The item is a folder — groups children. No target, no connector, no params. A folder is hidden when none of its descendants are visible (the runtime collapses empty folders). |
type set | The item is a leaf — points at a thing the user can open. Must have a target. |
Folders can nest indefinitely. Leaves can't have children — they're terminal nodes.
The four leaf kinds
type | Opens | target is | connector |
|---|---|---|---|
query | A screen (TableView) — uses the screen wired to this query. | A SELECT query name (customers_get). | The connector that owns the query. Blank = the app's. |
endpoint | The HttpRunner — fires an API connector endpoint. | An endpoint name. | The API connector. Blank = the app's. |
dashboard | A dashboard (charts + KPIs). | A dashboard id (from [dashboards.*]). | Must NOT be set (dashboards live in their own flat namespace). |
page | A registered frontend route (a custom feature area, e.g. /nomaflow). | The route path. | Must NOT be set (the target is a route, not a connector resource). |
The schema validator enforces:
- A leaf needs a
target— saving fails without one. - A
dashboardorpageleaf with aconnectoris rejected (misconfiguration). - A folder with
target/connector/paramsis rejected (those fields only make sense on leaves).
Save and reload
The Save button validates the whole MenusFile (unique ids, parents exist, no cycles), writes menus.toml and triggers a hot reload. New menus appear in the top switcher immediately — no process restart.
The validation is strict on three things:
| Check | Why |
|---|---|
Every parent reference must point at an existing item. | A dangling parent would orphan the subtree. |
| No cycles (a parent chain that loops). | An infinite-loop guard. |
No duplicate id within the same menu. | Children reference parents by id; duplicates break the link. |
Cross-menu duplicates are fine — [menus.crm.security] and [menus.nomasx1.security] coexist without conflict.
How a connector becomes an app
A connector becomes visible in the top app switcher only when both conditions are true:
- A menu exists —
[menus.<connector>]is set up in Settings → Menus. show_in_switcher = trueon the connector — set in Settings → Connectors → <connector> → Settings.
Miss either one and the connector exists but doesn't appear in the switcher. Setting up the menu first and then ticking show_in_switcher is the usual order.
Note: the Connectors page's own Apps / Data sources grouping is based on menu existence alone — connectors with a menu but show_in_switcher = false still show under Apps there. That grouping is an internal Settings-UI affordance; the user-facing top switcher is what show_in_switcher gates.
The next page covers the wiring from both sides.
Permission pruning
When the user opens the app, GET /api/menus returns the tree filtered to what the user can run:
- Each leaf's underlying permission (
sql:<connector>:<target>forquery,api:<connector>:<target>forendpoint, see Permissions and roles) is checked. - Items the user lacks permission for are dropped.
- A folder with no surviving children is collapsed away.
- Items with a
rolesfilter are kept only when the user's roles intersect the list.
A user sees only the parts of the menu they can actually use — no greyed-out items, no clicks on dead-end links.
What you actually do — quick map
| Goal | Read |
|---|---|
| Make a connector show up as an app in the top switcher. | Make a connector an app. |
| Build the tree — folders, leaves, drag/reorder, indent. | Build the tree. |
| Pick the right leaf type (screen / endpoint / dashboard / route). | Item types. |
| Restrict items to specific roles. | Permissions and roles. |
| Add icons + per-language labels. | Translations and icons. |
What's next
- Make a connector an app — wire
show_in_switcher,homeand the menu. - Concepts → Menus — the deep reference.