Skip to content

Deployment

Deploying a Laravel application is a solved problem, and this page does not restate it. There is nothing here about servers, domains, TLS, queues, or a CI provider — those are Laravel's concerns and they are documented better elsewhere.

What follows is the short list of things that are different because of this stack: four of them, each one something you would otherwise meet as a failure at the worst possible moment.

1. The machine that runs composer install needs your install token

Core comes from the private registry at packages.pragmatiqu.io, not from Packagist, so the build machine needs credentials just as your laptop does. It is the same line you ran when you installed Core:

bash
composer config --global http-basic.packages.pragmatiqu.io [email protected] YOUR-INSTALL-TOKEN

On a CI runner with no home directory to write into, pass the same pair through Composer's environment variable instead:

COMPOSER_AUTH={"http-basic":{"packages.pragmatiqu.io":{"username":"[email protected]","password":"YOUR-INSTALL-TOKEN"}}}

Never commit the token. --global writes ~/.composer/auth.json, which is outside your project; the environment variable belongs in your CI provider's secret store.

The error does not mention credentials

This is the step that stops a first pipeline. The registry answers an unauthenticated request with 401, so Composer either asks for credentials, which a CI runner cannot answer, or reports that the repository URL requires authentication. If the build instead ends with "Could not find a matching version of package laravelui5/core", check both halves: the credential, and the repository entry in composer.json.

2. Compile the OData schema before you ship

At runtime, discoverModel() reads your tables and your models to build the entity data model. That is exactly what you want in development and exactly what you do not want on a production boot.

bash
php artisan odata:cache

This writes plain PHP classes into an Edm/ directory beside each OData service, and you commit them. Deploy the generated classes like any other compiled asset.

The command refuses to run in production or staging, on purpose — it needs the database in order to read the schema, and a production box should not be doing that. Generate it where you can inspect the result, commit, then deploy.

Re-run it whenever the shape of a service changes: a new column, a changed cast, a new discoverModel() call, a new entity set. A stale Edm/ is a $metadata document that no longer matches the database.

3. The version in the URL is a cache-buster

Every app is served under its namespace and version:

/ui5/app/com/example/[email protected]/index.html

That segment is not there so you can serve two versions side by side — you cannot. Ui5Registry keys artifacts by namespace alone; the version is read off the artifact and stamped onto the address on the way out. There is exactly one live version of an app.

It is there because browsers hold on to what they have cached. Ship a fix to your front end, bump VERSION on the app class, and the address changes — every cached copy is orphaned, which is precisely what you want.

So bump it deliberately. A bump breaks every URL that pointed at the old address, including bookmarks and anything a colleague pasted into a chat. That is the feature, and it is also the reason not to do it casually.

4. The auth turnstile arms itself when a login route appears

Core ships an authentication turnstile on the ui5/ and odata/ route groups. It stays inert until all three of these hold:

ConditionDefault
config('ui5.auth_enabled') — from ENABLE_AUTH_4_UI5true when the variable is unset
the module's requiresAuth()true on AbstractUi5Module
the host has a route named loginabsent until you add one

Two of the three are already true in a stock installation, so the gate closes the moment your host gains a login route — which is what happens when you install a package like laravelui5/auth, or add Breeze, Jetstream, or a login route of your own.

That is usually what you want in production. It surprises people in development, where an app that worked a minute ago suddenly redirects to a sign-in.

Set the switch explicitly, in both directions

Relying on the default means the gate arms itself the day someone adds a login route for an unrelated reason. Put the value in .env and in .env.example, so the next person inherits the decision rather than the surprise.

ENABLE_AUTH_4_UI5=false

Core makes no identity decisions either way — it reads a boolean, and passes or bounces. Who may sign in, and what they may then do, is your host's business.

The release ritual

Deployment of a change to a UI5 app is four steps, in this order:

  1. Bump VERSION on the app class if anything the browser caches has changed — views, controllers, the manifest, bundled assets. Leave it alone for a pure backend change.

  2. php artisan odata:cache if the schema moved, and commit the generated Edm/.

  3. Commit and tag — and be clear about what you are tagging.

    A self-contained app is two things sharing one working tree. The host is the Laravel application: your .env, your routes, your migrations, the thing you deploy. The package is ui5/Showcase — an application that lives inside the host and is required through Composer like any other dependency.

    While the package is still a path repository, the two are the same commit and there is nothing to decide:

    bash
    git add -A
    git commit -m "Ship: <what changed>"
    git tag v1.1.0
    git push --follow-tags

    The moment that app has to run somewhere else — a second host, a customer installation, a colleague's machine — the package needs a repository of its own to be required from. The host keeps its own history; the package gets a target repository, and a split job on the host mirrors ui5/Showcase/ into it.

    From then on the tag belongs to the package rather than to the host, and it says which package:

    bash
    git push origin main
    git tag showcase/v1.1.0 && git push origin showcase/v1.1.0

    The prefix names which package inside the host is being released. The split strips it and pushes a plain v1.1.0 to the package repository, which is what Composer resolves against. One host can carry several packages this way, each with its own prefix and its own release history — which is the point: the host is where you work, the package repositories are what others consume.

    Two things that bite

    Push the branch before the tag. A bare git push origin <tag> does not carry the commit the tag points at, so the split has nothing to work from.

    Never delete a published tag to re-cut it. Composer caches versions by commit SHA in every vendor directory that ever installed the package; recreating a tag with different content breaks consumers silently. Ship a patch version instead.

    Setting the split job up — the workflow, the first push, registering the package with a repository Composer can see — is a topic of its own and not part of this page.

  4. Deploy, with the token available to whatever runs composer install.

The tag matters more here than in an ordinary Laravel app, because the version in the URL and the version in the tag should be able to answer the same question: which build is the browser holding?

What this page does not cover

Web server configuration, TLS, queue workers, storage drivers, php artisan optimize, zero-downtime strategies and rollback. All real, none of them specific to LaravelUi5, and all documented in Laravel's own deployment guide.

What's next?

  • Configuration — every key in config/ui5.php, including auth_enabled
  • Ui5App — where VERSION lives and what the @{ver} coordinate means
  • OData ServicediscoverModel(), configure(), and what odata:cache compiles