Hot Deploy & Live Reload
- Overview
- Dev Loop CLI
- Spring Boot
- JRebel
- HotswapAgent
- Jetty
- TomEE
- Designed for AI Coding Agents
- Prerequisites
- Installing the CLI
- The Cycle
- Commands
- What Apply Reports
- Limitations
- Configuration
- Troubleshooting
- Further Detail
- Related Topics
|
Note
|
Preview Feature
The Dev Loop CLI is a preview feature. This means that it is not yet ready for production usage and may have limitations or bugs. We encourage you to try it out and provide feedback to help us improve it. |
vaadin-dev is a command-line tool that owns your application’s edit-to-running-app loop: it compiles changed sources in the background, hot-swaps or restarts, pushes changed stylesheets and themes into the open page, and then tells you what actually happened. One command answers the question every other approach leaves you guessing about: is my last change live?
|
Note
|
Two different things share the vaadin-dev name. This page is about the CLI script, installed at .vaadin/vaadin-dev in your project. It requires the com.vaadin:vaadin-dev Maven dependency, which is a separate thing — the optional development-tools artifact described in Development Mode. In prose below, "the dev loop CLI" means the script.
|
Designed for AI Coding Agents
The dev loop CLI exists because AI coding agents are bad at the part of development that happens after the edit. An agent changes a view, rebuilds, relaunches the application, takes a screenshot, and then reasons about a picture to decide whether its change took effect. That is slow, and it is frequently wrong.
apply replaces all of it. An agent batches its edits, runs one command, and reads a verdict from the exit code:
Source code
bash
.vaadin/vaadin-dev applySource code
change-set: 2 file(s): src/main/java/com/example/TaskListView.java, src/main/frontend/themes/app/styles.css
compiling → runtime → Stable (1.2s)
hot-reload: redefineClasses(1); onHotswap completed=trueExit code 0 means the change is live, 1 means it failed, and 4 means a newer apply took over. No screenshot parsing, and no relaunch.
Two design properties make that verdict worth trusting:
Exactly one gate decides. While the daemon owns your application, Flow’s own save-triggered watchers are suspended. Nothing else pushes a stylesheet or refreshes a component behind your back, so one command decides when a change goes live and one command reports on it.
It never over-claims. A successful class redefinition is not proof that a change is visible. If Flow had no component to refresh, if an entity mapping is involved, if a proxied Spring bean changed shape, or if a frontend import moved, apply says so and escalates to a restart instead of reporting Stable.
Developers get exactly the same commands. Nothing on this page is agent-only.
|
Important
|
A long-running daemon owns your application’s process, and it starts on first use. Don’t start the application through Maven ( |
Prerequisites
-
Java 21 or newer. A floor, not a preference: the daemon drops any candidate JVM below 21, even if your project targets an older release. Your code is still compiled at the release your project declares.
-
A JetBrains Runtime, strongly recommended. Enhanced class redefinition is a JVM feature that only a JetBrains Runtime provides. On a stock JDK, structural changes are rejected and the dev loop escalates to a restart — nothing is broken, but you get fewer hot swaps. Candidates are discovered in
~/.jdks,JAVA_HOME, andJDK_HOME. -
Maven, as a wrapper next to your application or
mvnon the path. -
Network access while installing, to download the HotswapAgent the dev loop runs on. The install goal fetches it once per machine, and after that nothing in the loop needs the network again.
-
The
com.vaadin:vaadin-devdependency, declared optional so it can never reach a production build. Generated starters already declare it:Source code
XML
<dependency> <groupId>com.vaadin</groupId> <artifactId>vaadin-dev</artifactId> <optional>true</optional> </dependency>
Maven projects only. There’s no Gradle equivalent yet.
Installing the CLI
Run the install goal on the module that holds your application — in a multi-module reactor, that means the application module and not the root:
Source code
bash
mvn vaadin:install-dev-cliThe goal is unbound, so it never runs as a side effect of a normal build. Run it once per project, and again after a Vaadin upgrade.
It writes the CLI into .vaadin/ (with .ps1 and .cmd launchers for Windows) and instructions for AI coding agents into .agents/skills/vaadin-devloop/ and .claude/skills/vaadin-devloop/. Commit all of it, like mvnw — it’s project tooling, and the point is that every developer and every agent on the repository gets the same instructions. The goal rewrites these files whenever they differ from the shipped copy, so add your own skill beside them rather than editing them.
The goal also downloads HotswapAgent into ~/.vaadin/devloop, which is the one asset the dev loop fetches rather than resolving from a Maven repository. It’s provisioned here, rather than by the first .vaadin/vaadin-dev start, so that an image built with network access can be developed in without it — a container or a sandboxed agent environment would otherwise install cleanly and then fail to start the loop at all. The download is checksum-verified against a pinned version, lands outside the project, and serves every application on the machine, so it happens once and never again.
If the machine running the goal has no network, install the project files alone:
Source code
bash
mvn vaadin:install-dev-cli -Dvaadin.devcli.skipHotswapAgent=trueThat gives up the guarantee the provisioning exists for: the first start then downloads the agent itself and needs the network to do it. Dropping the release asset into ~/.vaadin/devloop/ by hand works too — whatever is there is checksum-verified either way.
An agent that reads the installed skills learns the cycle, the commands, and how to interpret every outcome. Two things the skills expect but don’t install: a browser automation tool such as a Playwright MCP server, and the Vaadin MCP server.
The Cycle
Source code
bash
# 1. Ask what is running. This costs milliseconds.
.vaadin/vaadin-dev status
# 2. If it says stopped, start the application. Blocks until it serves or fails.
.vaadin/vaadin-dev start
# 3. Open the application in the browser now, and keep the page open.
# 4. Edit Java, CSS, and frontend files. Batch the edits.
# 5. Make them live. The exit code is the verdict.
.vaadin/vaadin-dev apply
# 6. Verify what changed, then report it as working.A cold start takes roughly 30 seconds. Later commands are fast, because the daemon and the application both stay up. The application serves on http://localhost:8080 unless server.port says otherwise.
Open the page before the first apply, not after. A stylesheet or theme push has somewhere to land only if a page is already connected. With none, apply reports the file copied to the classpath and says nothing about a push — honest, but not the answer you wanted.
Batch the edits. apply finds the change-set itself by scanning the sources of every module in the loop, so run it once after a batch of edits, not once per file.
Verify what actually changed. A change with a visual surface — a view, component, layout, theme, or stylesheet — needs the browser, because nothing else proves the UI renders what you intended. A change with none — a service, a repository, a formatter, a configuration value — is answered by `apply’s verdict and the project’s own tests.
In a Maven reactor, every module your application depends on is in the loop too, so an edit in a sibling library reaches the running page without a rebuild. status names those modules; a module it doesn’t name is invisible to apply. A reactor holding several Vaadin applications is driven by the same script with --app, as in .vaadin/vaadin-dev --app ../admin apply.
On Windows a checkout carries no executable bit, so the Bash script may refuse to run:
Source code
bash
.vaadin/vaadin-dev applybash
bash
bash
bash
bash
Every command, option, and exit code is identical whichever launcher you use.
Commands
status [--json]-
Reports whether the application is up, which modules are in the loop, the state of the last transaction, and any errors the application has logged since the last
apply. Starts no JVM, so it costs milliseconds. start-
Launches the application in development mode. Blocks until it’s serving or has failed; a failure names the reason from the application’s own log.
apply [--json] [--no-restart]-
Makes the edits on disk live. Blocks until the outcome is terminal.
--no-restartstops after the compile gate, which is useful when you want compile errors without touching the running application. stop/restart/shutdown-
Stop the application; stop and start it again; or stop the daemon along with the application it owns. Use
restartafter changing anything the running JVM can’t absorb, such aspackage.jsonorvite.config.ts. Useshutdownif the daemon seems wedged — the next command starts a fresh one. --help-
The full usage text, including every option and environment variable. Answers without starting a daemon, which is what makes it useful when the loop isn’t working.
The exit code is the contract, and means the same thing on every platform: 0 the change is live (or there was nothing to do), 1 it failed, 4 a newer apply superseded this one. Usage and internal errors use 64, 70, and 77.
What Apply Reports
apply prints a phase line and then one line per part of the change. Stable means the change is live and the application is consistent:
Source code
compiling → runtime → Stable (1.2s)
hmr: 1 resource(s) copied, pushed 1 stylesheet(s) in place
hot-reload: redefineClasses(1); onHotswap completed=trueAn hmr: line reports hot module replacement — the change reached the open browser session without restarting the server, so you keep your session and your navigation state. A hot-reload: line is the Java counterpart, where new bytecode is swapped into the running JVM.
Broadly: stylesheets, themes, and most view and component code go live in place, with no reload. Structural Java changes, entity mappings, application.properties, bundled frontend files, and frontend annotations such as @JsModule need a restart, which apply performs itself and names the reason for. A compile failure reports file, line, and column, and leaves the running application on its last good bytes.
Two outcomes are worth recognizing, because neither is the success it resembles:
Stablewith anapp log:line under it-
The bytes are live and the code then threw. Read the error before reporting the change as working.
hmr: … no browser connected-
The file reached the classpath, but no page was open to push it into, so nothing went live in a page. Open the application and run
applyagain.
--json returns the same information as a single object, for tooling.
Limitations
Most of what the dev loop can’t do it reports honestly, by escalating to a restart rather than claiming success. Two exceptions can mislead you, and are worth knowing before you trust a Stable:
|
Important
|
Annotation processors don’t run. A source file recompiled by the dev loop loses every member and class a processor would have generated. A project using Lombok, MapStruct, or Dagger needs An edit that changes what a class promises its callers isn’t supported. Only the change-set is recompiled, so callers you didn’t edit keep the bytecode they were compiled with. Rename or re-sign a method, or change a |
Also worth knowing: JPA entity mappings never hot-reload, a structural change to a proxied Spring bean (including a Spring Data repository) always restarts, and in the default dev-bundle mode any non-theme frontend edit restarts the application to rebuild the bundle, which takes tens of seconds.
Configuration
VAADIN_DEV_DAEMON_OPTS passes JVM options to the daemon, and every vaadin.* and spring.* property among them is passed on to the application. It’s read only when a daemon starts, so run shutdown first when changing a value. --help lists the rest.
The application is launched with no program arguments, so a Spring profile goes in as a property rather than --spring.profiles.active=dev:
Source code
bash
.vaadin/vaadin-dev shutdown
VAADIN_DEV_DAEMON_OPTS="-Dspring.profiles.active=dev" .vaadin/vaadin-dev startA property set this way lives as long as the daemon and appears in no file, so it’s for steering one local run. If a profile is what the project normally runs under, spring.profiles.active in application.properties is the better answer.
To develop frontend files actively, turn on hot deploy the same way, with -Dvaadin.frontend.hotdeploy=true. Vite then applies frontend edits when you save them, and apply says so instead of restarting. See Configuration Properties.
Troubleshooting
The application’s own output is in target/devloop/app.log, and the daemon’s in target/devloop/daemon.log. Between them and status, most questions answer themselves.
this project does not depend on the dev-loop daemon-
Add
com.vaadin:vaadin-devas an optional dependency, as shown under Prerequisites. - The application fails to start
-
startexits1and names the reason, with the tail ofapp.logunder it. A taken port or a bad configuration value shows up there. - A change didn’t take effect
-
Check
status. If it reports the application stopped or crashed, the log says why. Ifapplyreported a restart, reload the page. - More edits restart than you expect
-
Check the daemon log for which JVM was chosen. Without a JetBrains Runtime, structural changes can’t be hot-swapped at all. See Live Reload with HotswapAgent for how to install one.
- The daemon seems wedged
-
Run
shutdown. Any later command starts a fresh daemon.
Further Detail
The full reference ships with the CLI, because it’s written for the agents that consume it most:
-
.agents/skills/vaadin-devloop/SKILL.md— the cycle, the command set, and every outcome explained in one line each. -
.agents/skills/vaadin-devloop/reference.md— the completeapplyoutput vocabulary, a table of which edits need a page reload, pom and classpath semantics, the--jsonschema, browser assertion patterns, environment variables, and what to do when the loop goes wrong. -
.vaadin/vaadin-dev --help— every command, option, environment variable, and daemon property.
Related Topics
-
Hot Deploy & Live Reload — the other ways to get changes into a running application
-
Live Reload with HotswapAgent — installing a JetBrains Runtime
-
Development Mode — the
com.vaadin:vaadin-devdependency -
MCP Server for Vaadin — documentation lookup for AI coding agents