Architecture
Core draws a bracket around a UI5 app and the backend that serves it — the entity sets it reads from, the actions it writes through, the cards and reports it renders — and treats the two halves as one thing.
The consequence is the part worth remembering: that bracket is a Composer package. A feature is not a folder in your application and a folder in a front-end repository that have to be deployed in step. It is one package with a name, a version and a require line.
ui5:app --create scaffolds it that way. The module gets its own composer.json — the package name, a PSR-4 root over src/, and its service provider under extra.laravel.providers — and the host takes it the way it takes any package:
composer config repositories.invoicing path ui5/Invoicing
composer require acme/invoicing:@devPlus one line in config/ui5.php naming the module class. That line stays manual on purpose: which modules a host serves is a product decision, not a directory listing.
So a module can be developed inside the host, split out of it later, versioned on its own, and required by a second host without anything being rewritten. It is also why every URL carries an @{version} coordinate — the address names a release of the bracket, not just a path.
Ui5Action.Why a package, and not a folder
Laravel is excellent at consuming packages — auto-discovery has made composer require the whole installation since 5.5 — and since the official package skeleton it is good at authoring them too. The case it has never had an opinion about is the one in between: an application that wants to be several packages. The framework's package chapter is about libraries you publish for other people; splitting your own application along its domains is not in it.
The ecosystem filled that gap, which is the surest sign the gap is real: nwidart/laravel-modules for full module management, InterNACHI/modular for the variant that stays closest to Laravel's own conventions, plus a shelf of courses and books on package development.
Core stands in that tradition rather than beside it. A module is a Composer path repository, discovered through extra.laravel.providers — the same mechanism InterNACHI/modular settled on, and deliberately not a new one. What Core adds is the other half of the bracket: the module carries a UI5 front end as well as its backend, and one registry gives both an address.
Why the front end belongs in the bracket
The module systems above draw their boundary through the PHP. That gives you code ownership — this domain's routes, models and migrations live together — and it stops there. Compose two such modules into one application and you get two sets of views that merely share a layout file. Whether they look like one product is nobody's contract; it is a standing agreement between developers, maintained by hand, and it degrades the moment a second team joins.
Putting the UI5 half inside the bracket changes what a module can promise, because UI5 does not ship controls alone. It ships a design system — the SAP Fiori guidelines: layouts, interaction patterns, iconography, theming, accessibility, i18n. A module's screens are not styled to match the others; they are built against the same specification the others were built against. Coherence stops being coordination work and becomes a property of the stack.
That is the level the composition moves to. Two modules, written by two teams — or bought from two vendors — drop into one host and behave like one application: same shell, same navigation, same controls, same keyboard and screen-reader behaviour. The backend boundary makes them deployable separately; the design contract makes them presentable together. Neither half alone gets you a composable product.
It also means the two halves are modular in the same currency. A UI5 library is a versioned, distributable unit in UI5's own world, exactly as a Composer package is in PHP's. Core's contribution is to make them one unit with one version, so there is no pair of releases to keep in step.
What sits inside the bracket
Everything in there is an artifact: a PHP class that declares what it is and points at what does the work. They form a short hierarchy.
The container. A Module groups artifacts under one namespace root and one bounded context. It is not addressable itself — nothing is served at a module's URL — and it is where #[Slot] declarations live.
The addressable roots. An App is the root for user-facing content, and it is an OData v4 service: AbstractUi5App extends ODataService, so the app's read surface comes with it. A Library is the other root — a UI5 library project brought in to be shared across apps.
Everything else hangs off the app and takes its context from it: Dashboards and the groups that compose them, Cards, Tiles, Charts, Reports, Actions and Resources. Two further types — dialogs and value helps — have their seat in Core's module interface and their base classes in the SDK.
The full catalogue, with what each type is for, is in the Backend Overview.
Core keeps a registry of all of it, and that registry is what turns a declaration into an address. The rest of this page is how.
Declared, not wired
An artifact is a plain PHP object. Its identity lives in class constants, its behaviour in a handler or provider it points at, and anything the framework needs to know about it is stated in attributes:
#[Access(ability: 'invoicing', note: 'Open the invoicing app.')]
class InvoicingApp extends AbstractUi5App
{
public const string NAMESPACE = 'com.acme.invoicing';
public const string VERSION = '1.0.0';
// …
}Registration is two steps and there is no third: the artifact is returned from its module (getCards(), getActions(), getReports(), …), and the module class is listed in config/ui5.php under modules. Nothing is wired in a route file, a service provider or a front-end config. Attributes declare; classes do.
What Core derives
Once an artifact is registered, the registry knows it by namespace and version, and the framework answers for it:
| Derived | What it means |
|---|---|
| Routes | Every artifact type has one fixed route shape — app/{ns}@{ver}/index.html, card/{ns}@{ver}/manifest.json, resource/{ns}@{ver}, api/{ns}@{ver}/{uri?} for actions. You never register a route. |
| The app manifest | The laravel.ui5 block of a served manifest.json — the addresses of the app's actions and resources, its settings, the infrastructure its modules contribute — is assembled from the registry, not authored. |
| The OData service | An app is an OData service; its entity sets are declared in configure() and served under /odata/{ns}@{ver}/. |
| Addresses | The @{version} coordinate is part of every URL, which makes a version bump a deliberate, cache-busting act. |
The dotted namespace becomes slashes in a URL: com.acme.invoicing is addressed as com/acme/invoicing.
Reading and writing use different doors
This is the division a newcomer most often gets wrong, so it is stated plainly:
OData is read-only. The data layer serves queries — $filter, $expand, $select, paging — and it never accepts a write. There is no POST to an entity set.
Every write is a Ui5Action. An action is an addressable, versioned artifact with its own handler, its own FormRequest for validation, and its own #[Act] gate. It is invoked from the front end by namespace, not by URL.
The split is deliberate. A read is a question the client may shape freely; a write is a named business operation with a contract, an authorization and a transaction. Giving them the same door would mean giving the client the pen.
Two halves, and how the UI5 half is served
The front end is an ordinary OpenUI5 project — your own, built with the UI5 tooling, living in its own source tree. Core does not generate it and does not own it. What Core owns is how it reaches the browser, and that differs by source strategy:
- Workspace — the development shape. The UI5 dev server serves the source; the app boots without a preload bundle and the framework comes from the proxied dev server. Edit a view, reload, see it.
- Package / self-contained — the shipped shape. The built
dist/is imported into the module'sresources/, travels inside the Composer package, and is served from there with aComponent-preload.jsand the framework from the CDN.
Same artifact, same URL, two ways of getting the bytes there. Nothing in your PHP changes between them.
Where Core ends
Core is stateless about your organisation. It knows artifacts, routes, manifests and the execution pipeline. It knows nothing about users, tenants, partners, roles or stored configuration — not because that was forgotten, but because a kernel that assumes an identity model cannot be used by an application that has a different one.
The vocabulary reaches a little further than the implementation, and that is on purpose: a module can return getDialogs() and getValueHelps(), but the base classes for those two live in the SDK. Core defines the seat; the SDK fills it, and adds the parts that need state — stored settings, abilities, partners, the shell.
Where to go next
- Backend Overview — the file and naming conventions, and how the artifact types relate.
- Execution Model — what happens during one request: artifact resolution, parameters, settings, validation, invocation.
- Ui5Module — the container everything hangs off.
- Quickstart — the same picture, assembled and running.