Capability Mini-Apps
Sooner or later you have to answer a question UI5 never asks out loud: how big is an app?
Coming from Laravel, the instinct is clear. One application, many routes, and a guard on the routes that may not be reached by everyone — Route::middleware('can:approve-timesheets') is muscle memory. Carried into UI5 that becomes one large app with a router, an ability looked up per route pattern, and a redirect when the check fails.
It works, and it is the wrong cut. This page is the rule that replaces it, and it is the kind of thing you would otherwise arrive at yourself around the third or fourth app.
The rule
An app is a capability. If not everyone who may open the app may do this, it is its own app.
So instead of one timesheet app with three views:
TimesheetEntryApp — everyone books their hours
TimesheetApprovalApp — team leads approve
TimesheetExportApp — accounting exportsThree apps, three modules, three #[Access] abilities. Each one carries its own router, its own views, its own registration.
The test is one sentence long, and it is worth applying literally: if the set of people who may use this differs from the set who may open the app, it is a different app. Not "if it is a different screen" — screens may differ freely inside one app. The cut follows permission, not topic.
Where the gate sits
On the app class, in the backend:
#[Access(
ability: 'approveTimesheets',
role: AcmeRole::Accounting,
note: 'Approve submitted timesheets.',
)]
class TimesheetApprovalApp extends AbstractUi5App { /* … */ }That one declaration does four things:
- Every request to the app is refused with 403 if the actor does not hold the ability —
CheckAuthMiddlewarechecks it per request, before your code runs. - The app disappears from the shell navigation.
- It disappears from the command palette.
- Its tile is not rendered on the Launchpad.
An actor who lacks the ability does not get a polite refusal. They get an application that, as far as they can tell, does not exist.
What that buys you inside the app
This is the part that is hard to appreciate before you have built the other kind.
The app may assume its user. Inside TimesheetApprovalApp there is no question left about whether this person may approve — they are here, so they may. No router guard, no ability lookup on navigation, no redirect after the fact, no message box explaining a refusal.
Navigation is preventive, not corrective. There is no state in which something forbidden was reachable and then taken back. For the user that is the difference between an application and an application that keeps apologising; for anyone probing, it is the difference between a hidden surface and a surface that announces itself by refusing.
Complexity stops scaling with roles. An app that serves four roles otherwise carries four views of itself — conditional navigation, conditional views, conditional actions — and every new role touches all of them. A capability app serves one.
And the cut is real, not notional. A capability that is its own app is also its own module: its own Composer package, its own version, deployable on its own. You can hand it to another host, or withhold it, without touching the others.
The other gates
#[Access] answers may this actor open it. Three more ability types answer the other questions — Act for operations, See for individual UI elements, Read for the data endpoint. They are the subject of Permission Levels, and two of them are worth knowing here:
Act is checked on the server, always. The client may ask, to keep a button out of the way:
if (LaravelUi5.can("act/exportTimesheet")) { /* show the button */ }…but the answer is a hint for the interface, never the gate. The action refuses on its own.
See you do not check at all. Element visibility is resolved on the server and injected into the app manifest, and UI5 hides the controls natively — see View Visibility.
When routes still differ legitimately
Splitting by capability does not mean one route per app. Inside an app, routes are free, and there are cases where a route-level distinction is exactly right:
- wizard sub-steps — the same permission, a different stage
- feature flags — a rollout question, not an authorization one
- experimental segments — visible to a group you are piloting with
The line is simple: if the distinction is about who may, it is an app boundary. If it is about where the user is, it is a route.
What it costs
More apps. That is the honest trade, and the two things that absorb it:
Shared code goes into a library, not into a common parent app — the same controls, formatters and base controllers, imported by each capability app.
The shell ties them back together. Navigation, the command palette and the Launchpad present the apps an actor may open as one product, and Weave opens doorways between them where a record in one app has a continuation in another. The user does not experience three apps. They experience three things they are allowed to do.
See also
- Permission Levels — the four ability types and where each is enforced
- View Visibility — how
Seeis applied server-side - Domain Model — abilities, roles, grants and scopes
- Ui5Module — why a capability is also a package