Deployment
A LaravelUi5 deploy is an ordinary Laravel deploy plus one command chain. There is no queue worker to run, no scheduler entry, no separate Node process in production, and no service to keep alive — the SDK compiles at deploy time and reads files at runtime.
What follows is the runbook, then the two things that actually go wrong.
The chain
php artisan migrate --force
php artisan ui5:sync
php artisan ui5:cache # after sync — it bakes the ability ids sync just assigned
php artisan ui5:nav
php artisan ui5:help --all
php artisan ui5:publish --force
php artisan config:cache && php artisan route:cacheFive of those are the SDK's and they are ordered, not arbitrary:
ui5:syncfirst. It turns your declarations into rows — artifacts, abilities, roles, settings, slots, Customizing catalogs. Everything after it compiles what it wrote.ui5:cacheandui5:navafter sync, because they bake the ids sync has just assigned.ui5:help --allwrites intostorage/, so it must run on the machine that serves the requests, or into shared storage.ui5:publish --forcelast of the SDK's. It deletes and re-createspublic/sdk— a dumb recursive copy.--forceskips the confirmation prompt; a non-interactive pipeline proceeds without it anyway, but passing it makes the intent explicit.What it publishes is not the shell bundle. That is served straight from the package at a URL carrying the SDK version — so a release changes the URL by construction, exactly as every artifact route does, and there is no cache to fight and nothing to keep in step. What
ui5:publishcopies is what the bundle references but does not contain: the SAP 72 fonts its stylesheet points at, and the fallback avatar. Skip it and the shell renders in the wrong font with a broken avatar.
On a first install one command comes before all of them:
php artisan ui5:intake --name="Acme GmbH" --email=office@acme.testThe system actor must exist before the first ui5:sync, because the settings worker stamps it as the writer on every seeded row.
What must be writable
| Path | Written by | Contains |
|---|---|---|
bootstrap/cache/ui5.php | ui5:cache | The compiled registry |
bootstrap/cache/ui5-nav.php | ui5:nav | The compiled navigation tree |
storage/ui5/help/ | ui5:help --all | Compiled help HTML, toc.html, index.json |
public/sdk/ | ui5:publish | Fonts and the fallback avatar — not the shell bundle |
None of these belong in your repository. All four are build output, and all four must exist before the first request — a missing help index is a broken F1, and a missing ui5-nav.php makes CachedNavigationService throw outright.
Which registry to bind
Either works, and the choice is a performance one.
// config/ui5.php
'registry' => \LaravelUi5\Sdk\Platform\SdkRegistry::class, // reflects per request
'registry' => \LaravelUi5\Sdk\Platform\CachedRegistry::class, // reads bootstrap/cache/ui5.phpOnly CachedRegistry reads the compiled bootstrap/cache/ui5.php; under SdkRegistry, ui5:cache produces nothing the application reads. CachedRegistry does nothing else: no module walk, no reflection, no fallback. It is the faster of the two and the one to bind when the artifact set is settled.
The catch it used to carry is gone. Until 1.2.0 the three build commands took whatever was bound and refused to run on the cached registry, so binding it cost you the ability to rebuild the very file it reads. ui5:sync, ui5:cache and ui5:nav now reach past the binding to the live registry on their own — a build step is not a request, and building the cache out of the cache would be circular anyway. Bind either one; the deploy chain below is unchanged.
Navigation is the one place where you do switch to the cached implementation, because there the command and the service are separate:
'navigation_service' => \LaravelUi5\Sdk\Shell\Navigation\CachedNavigationService::class,Run ui5:nav before the first request after that, or it throws.
The two things that go wrong
1. Code deployed, sync not run
A gate lives in two places: the #[Access] or #[Act] attribute in your code, and the row ui5:sync writes for it. Between deploying the first and running the second, the gate is declared and unresolvable — and the SDK refuses the request rather than guessing:
MissingModuleException— the artifact's module has nosdk_artifactsrow at all.MissingAbilityException— the module is synced, the declared ability is not.
Both are 500s, both are reported, and both name ui5:sync in the message. That is deliberate: an unresolvable gate is a deployment error, and the alternative is worse than an outage. Before 1.2.0 the second case resolved to "this artifact declares no gate" and the request was let through — a newly declared gate protected nothing, silently, for the length of the window.
So the chain above is not "code now, sync later": run ui5:sync in the same maintenance step as the code swap, before the new revision takes traffic. On a zero-downtime setup that means syncing against the new release before you flip the symlink, not after.
2. Help exists in the repository but not on the server
ui5:help --all writes to storage/, which is exactly the directory a deploy usually treats as persistent-and-untouched. If the command is missing from the pipeline, the help documents deploy with your code and the compiled output does not: F1 reports that help is unavailable, and the search index is empty.
Note that a bare ui5:help only validates and writes nothing. The flag matters.
Zero downtime
Nothing in the SDK holds process state, so the usual release-directory-plus-symlink shape works. Two rules:
- Share
storage/between releases, or runui5:help --allin every release directory. A sharedstorage/is simpler and matches Laravel's own convention. - Do not share
bootstrap/cache/orpublic/sdk/. They are per-release build output; sharing them lets an old revision serve a new revision's compiled registry.
Rolling back
Rolling the code back is not enough on its own, because ui5:sync has already reconciled the database to the new declarations — and reconciling means it deleted rows the new code did not declare.
So a rollback is a deploy:
# with the previous release checked out
php artisan ui5:sync
php artisan ui5:cache && php artisan ui5:nav
php artisan ui5:help --all
php artisan ui5:publish --forceMigrations are the part that does not roll back cleanly, which is the ordinary Laravel caution rather than anything the SDK adds: prefer additive migrations, and treat a destructive one as a one-way door.
A smoke test that is worth the two minutes
After every deploy, signed in as a real account:
- The Launchpad loads at
/ui5/dashboard(the route nameddashboard) and shows tiles. (Registry, navigation, abilities.) Cmd+Kopens and finds something. (Shell bundle, search route, discovery.)F1opens a help page. (ui5:help --all, the storage path.)- One list loads its data. (OData stack, the context binding — an empty list here is the symptom to take seriously, not a slow query.)
- One write succeeds. (Action dispatch, CSRF, the
#[Act]gate.)
Each step exercises a different half of the chain above, and between them they catch every failure described on this page.
What the SDK does not need in production
- No queue worker and no scheduled job — the SDK dispatches nothing and schedules nothing.
- No Node process. UI5 builds happen in development; production serves compiled assets.
- No websocket, no cache server beyond what your application already uses.
- No license check at runtime. The licence gates
composer require, not the running application.