Deploy to production
A Mateu app is an ordinary application of its framework — a Spring Boot jar, a Quarkus app, a Micronaut or Helidon jar — that also serves the renderer’s static assets and the Mateu action endpoint. Most of production is therefore what you already do for that framework. This page covers what is specific to Mateu.
1. Package
Section titled “1. Package”Package exactly as the framework expects; the renderer (mateu-vaadin or mateu-redwood) is a jar of static
assets on the classpath and travels inside your artifact.
| Runtime | Build | Run |
|---|---|---|
| Spring Boot (MVC / WebFlux) | mvn package | java -jar target/app.jar |
| Quarkus | mvn package (fast-jar) | java -jar target/quarkus-app/quarkus-run.jar |
| Micronaut | mvn package | java -jar target/app.jar |
| Helidon MP | mvn package | java -jar target/<artifactId>.jar (the Helidon parent drops the version from the name) |
Checks before you ship:
- Pin
mateu.versionto a release and keep everyio.mateuartifact on that same version. - The Mateu annotation processor ran: the jar contains one generated controller per
@UIclass (*MateuController). An app whose processor did not run boots and answers 404 everywhere; Spring MVC/WebFlux apps log “Mateu found no UI to serve” at startup. - Sample mode is off. A generated YAML-only app turns
mateu.sources.mockon in its main class until your API exists, and a generated static project ships-Dmateu.bundle.mock=truein.mvn/maven.config: both answer every REST source with itssample:data. Delete that line of the main class (or run withMATEU_SOURCES_MOCK=false) and that line of.mvn/maven.configbefore building for production. - Development mode is off: no
mateu.dev/MATEU_DEVin the production configuration (see Live reload). - For a multi-module app, the UI modules were compiled with
mateu-annotation-processor-indexerand are on the app’sannotationProcessorPaths(see service-owned UI modules). - Helidon MP: the app has a
META-INF/beans.xmlandjersey-media-json-jacksonon the runtime classpath — without them every route answers 404 or the UI renders empty.
A container image needs nothing special: a JRE 21 base image and the jar. The starters in
starters/ are a good base.
2. Paths, reverse proxies and load balancers
Section titled “2. Paths, reverse proxies and load balancers”The browser talks to two kinds of URLs:
- the app’s routes and its action endpoint, under the path of the
@UImount:@UI("/admin")serves/admin/**and posts actions to/admin/mateu/v3/…; - the renderer’s static assets under
/assets/**.
What follows from it:
- Serve the app at the root of its host (
https://admin.example.com/), or give the mount the public path you want (@UI("/admin")) and route both/admin/**and/assets/**to the backend. - A servlet context path (
server.servlet.context-path) or a proxy that strips a prefix is not supported: the generated page references/assets/…from the root, so the assets 404 behind a prefix. - Several Mateu apps behind one host: give each its own
@UIpath. They all load/assets/**from the same place, so they must run the same Mateu release — or live on separate hosts. - Long-running actions (
LongTask, streamed responses) use server-sent events: disable response buffering for the action endpoint on the proxy (proxy_buffering off;in nginx) and raise its read timeout above your longest task. - Sticky sessions are not needed — see the next section.
3. Stateless view models
Section titled “3. Stateless view models”Mateu keeps no UI state on the server between requests: the browser sends the component state with every action, and a routed view model is instantiated fresh on every request. That is what makes any instance able to answer any request, so you can scale horizontally behind a plain load balancer.
Keep it that way:
- Do not register a view model with mutable fields as a singleton bean. Leave it a plain class
(Mateu instantiates it and still injects
@Autowired/@Injectfields), or make the bean prototype-scoped. A singleton view model shares one user’s half-typed form with everybody. - Keep data in your stores and services, not in static fields of a view model (the in-memory stores of the starters and demos are for demos).
AutoCrudsubclasses that only holdfinalinjected services are stateless and can be singletons.
4. Authentication and authorization
Section titled “4. Authentication and authorization”Mateu does not authenticate users itself; it reads an identity the platform established:
- Login: put your identity provider in front of the app — an OIDC-aware gateway, Spring Security,
Quarkus OIDC… (the older
@KeycloakSecured, which has the browser log in against Keycloak, still works but is deprecated). - Authorization:
@EyesOnly,@ReadOnlyUnlessand@DisabledUnlesscheck roles, groups, scopes and permissions of the caller your framework authenticated (see Security) — never a token Mateu decodes itself. - Authenticate in your framework. Mateu does not authenticate: it takes roles only from the
principal your framework authenticated (Spring Security resource server, Quarkus OIDC, Micronaut
Security, MicroProfile JWT) or a
PrincipalResolveryou register. Without one, restricted UI is hidden for everyone — see Security. - A YAML-authored app restricts routes, menu items, actions and components with the same rules — see Permissions in YAML.
- Enforce permissions in your services too. Hiding a button is UX; the action method behind it must still check who is calling.
5. A static bundle on a CDN
Section titled “5. A static bundle on a CDN”Screens that need no server logic can ship as a static bundle — mvn package with the
mateu-bundle-maven-plugin’s bundle goal bound (a generated static project has it; the demos put
it behind a bundle profile, hence mvn -Pbundle package there) — and be served by any CDN or object store with no Mateu backend at runtime. See
Static bundle and 100% static UI.
For production:
- Serve
index.htmlwith no caching (or a short TTL) and the hashed assets with a long one (Cache-Control: public, max-age=31536000, immutable). - Configure the host’s SPA fallback — every unknown path answers
index.html— so deep links work. staticOnly(-Dmateu.bundle.static=true) fails the build if a route still needs a server: turn it on for bundles that have no backend.- The bundle calls your REST APIs from the browser: those APIs need CORS for the CDN’s origin,
and must never receive a secret the browser can read. Use a source in
proxymode (served through a Mateu backend, which injects${secret.…}server-side) for anything that needs a key. - Re-pointing a bundle to another environment means editing the source catalogue in
manifest.json— no rebuild of the screens — or rebuilding with-Dmateu.bundle.environment=pro(thestructureHashdoes not change).
Environments and secrets
Section titled “Environments and secrets”Keep one sources.yaml and one environment file per
deployment (specs/ui/environments/pre.yaml, pro.yaml), and select it with MATEU_ENVIRONMENT
(or -Dmateu.environment) on the server, -Dmateu.bundle.environment for a bundle. Environment
files hold base URLs, URLs, headers and the proxy flag — never a secret: write ${secret.X} and
set MATEU_SECRET_X on the server, which resolves it on the proxy leg only. A literal credential
header in an environment file is warned about at startup.
6. Content Security Policy
Section titled “6. Content Security Policy”The web renderers work under a CSP; the parts to allow:
script-src 'self'plus the CDN of your identity provider’s JS adapter if you use one (Keycloak).- No
'unsafe-eval': client-side expressions —${…}in labels, rules,@Disabled(expression), visibility rules — run in Mateu’s own sandboxed evaluator. Only a page that opts into RunJS (<meta name="mateu-allow-run-js" content="true">) needs'unsafe-eval', and only that page. style-src 'self' 'unsafe-inline'— web components set inline styles.connect-src 'self'plus every REST API your screens call directly (@RestOptions,@RestListing, sources in direct mode).- The Redwood renderer loads Oracle JET from Oracle’s CDN (
https://static.oracle.com): allow it inscript-src,style-src,font-srcandimg-src. img-src data:if you use@UploadableImage,@Signatureor@PhotoCapture(their values are data URIs).
Start in Content-Security-Policy-Report-Only mode and tighten from the reports.
7. CORS
Section titled “7. CORS”A browser only needs CORS when the page and the backend are on different origins — a static bundle on a CDN calling your APIs, a renderer served separately from its backend, a micro-frontend embedded in another product. A Mateu app served by its own backend (the normal case) needs none.
CORS is off by default on every adapter (Spring MVC, WebFlux, Quarkus, Micronaut, Helidon): only same-origin pages may call the Mateu endpoints. Allow the exact origins you need:
mateu.cors.allowed-origins=https://app.example.com,https://cdn.example.com# only if the browser must send cookies / Authorization with a credentialed requestmateu.cors.allow-credentials=true* is refused together with credentials. On Micronaut, a server bound to localhost also refuses
non-localhost origins unless micronaut.server.cors.localhost-pass-through=true. .NET: configure
ASP.NET Core’s CORS middleware for the Mateu endpoints; Python: add_mateu(app, cors_origins=[...]).
The MCP endpoint (POST /mateu/mcp) is likewise off unless mateu.mcp.enabled=true — turn it on
only where an agent should operate the app, behind the same authentication as the UI.
8. Before go-live
Section titled “8. Before go-live”-
mateu.versionpinned to a release; allio.mateuartifacts on it - Generated controllers present in the artifact; every menu entry loads
- Served at the host root, or
@UIpath +/assets/**both routed to the backend - SSE not buffered by the proxy (if you use long-running actions)
- No singleton view model with mutable fields
- Your framework authenticates requests (resource server / OIDC / JWT module); services check permissions
- CSP set (report-only first), CORS limited to the origins you need
- Static bundle: SPA fallback, cache headers,
staticOnlyon, no secrets in direct sources - Sample mode off (
mateu.sources.mock,-Dmateu.bundle.mock) and development mode off (mateu.dev)