Run any Java Swing application inside a modern web browser — pure HTML5, zero plugins, zero client-side installation.
Getting Started · Build · Upgrading · What's New · Architecture
Suramadu renders Java Swing applications into an HTML5 Canvas and delivers them to any browser over a WebSocket. Your application runs unchanged on the server; the browser only draws the window.
The project is a fork of WebSwing v20.2.5, the last release published under the GNU AGPL v3. It is maintained by Manticore Projects to keep that codebase running on current JDKs, with a focus on modern JDK compatibility, security hardening and build toolchain modernization.
Suramadu is not affiliated with, endorsed by, or supported by Webswing Limited or Webswing s.r.o.
The Suramadu is the bridge that crosses from Java to the island of Madura — 5.4 km of cable-stayed span across the Madura Strait, first proposed in the 1960s and finally opened in 2009. It seemed a fitting name for software whose whole job is carrying Java across to somewhere else.
Suramadu is licensed under the GNU AGPL v3. If you interact with a running instance over a network, you are entitled to the complete corresponding source of that instance under section 13 of the licence.
The complete corresponding source for every released version is in this repository. Releases are tagged; deployed instances link back here.
The clipboard and file-transfer round trips were reworked around modern browser APIs, so the
common actions — copy, paste, open a file, save a file — take one step instead of several.
Everything here degrades gracefully: on plain-HTTP origins and older browsers the previous flows
are kept as fallbacks, and the Java JFileChooser / Clipboard APIs are unchanged.
- Direct clipboard copy & paste. Copying inside the Swing application places the content on the
system clipboard in a single
Ctrl+C— no intermediate dialog, no second keystroke. The browser's asynchronous Clipboard API is armed within the copy gesture and resolved when the server's clipboard content arrives; in-app Copy buttons and multi-step copies write silently too. Pasting reads the local clipboard directly, with the permission prompt moved to session start instead of interrupting the first paste. On plain-HTTP origins the previous copy-bar / paste-dialog flow is used instead. Off switch:directClipboard: false. - One-step file open. When an application requests a local file, the browser's native file
picker opens immediately; the chosen file uploads and the
JFileChooserconfirms itself, so the user no longer steps through pick → upload → select → Open. Files can also be dropped anywhere in the window while a dialog is open. A completed upload now auto-approves a visible (server-browsing) chooser as well (webswing.uploadAutoApprove, default on). - Per-dialog local / server choice. An application can decide, per dialog, whether a chooser
browses the client or the server — no global switch required — via
JFileChooserclient properties:webswingFileDialogMode="local"(native picker) or"server"(server file system), andwebswingUploadAutoApprove(Boolean). - Native Save As. On Chromium, downloads stream to a user-chosen location through the browser's save dialog; other browsers keep the default download.
- Installable app window (PWA). A web manifest lets the application install as a standalone
window — no tab strip or address bar, a taskbar icon per deployment. In fullscreen on Chromium,
system shortcuts (
Ctrl+W,Ctrl+T) reach the Swing application instead of the browser via the Keyboard Lock API. - Accidental-close protection. Closing the browser tab or window during a live session asks for
confirmation; navigations the application itself triggers (shutdown, logout, redirects) never
prompt. Off switch:
warnOnClose: false.
- No more UI corruption on slow connections. On a slow or congested link, the browser could
draw parts of the UI with the wrong colours, shapes or text. Cause: the server's DirectDraw
constant cache recycled ids within a single large frame, while the browser applies a frame's
constants in a different order than the server allocated them. The cache now never evicts an
entry used in the current frame and grows instead (
LRUDrawConstantPoolCache). - Slow sessions no longer collapse. After 5 s without a paint acknowledgement the server used to reset its cache and re-send everything, fonts included, to exactly the client that was already too slow: a feedback loop down to under 1 frame per second. The timeout now resumes sending without a reset.
- Cache reset race fixed. A browser resize, reconnect or recording start could reset the cache while a frame was being encoded. Resets now happen only between frames.
Recommended standard OpenJDK distributions: Eclipse Temurin, BellSoft Liberica, or Amazon Corretto.
| JDK | Status | Temurin Support | Notes |
|---|---|---|---|
| 17 (LTS) | 🔶 Supported | Oct 2027 | Toolchain of the build |
| 21 (LTS) | ✅ Supported | Dec 2029 | |
| 23 | ✅ Supported | ⛔ EOL (Mar 2025) | Non-LTS, 6-month lifecycle |
| 25 (LTS) | ✅ Supported | Sep 2031 | Current LTS |
| 26 | ✅ Supported | ⛔ EOL (Sep 2026) | Non-LTS; requires --sun-misc-unsafe-memory-access=allow |
| 27 | ✅ Supported | Mar 2027 | Non-LTS; dedicated *27 toolkit variant, selected automatically |
All internal APIs adapted for the post-JDK-11 module system — no --illegal-access=permit, no -noverify required.
JavaFX applications require JDK ≤ 26 — there is no JavaFX toolkit variant for JDK 27 yet; the launcher refuses the combination explicitly.
Suramadu 27.0 adds a dedicated toolkit variant (suramadu-app-toolkit-java27) for JDK 27,
where the applet-era sun.awt.AppContext was removed from the JDK: SunToolkit.postEvent()
lost its AppContext parameter, SunToolkit.getDataTransferer() moved to
sun.awt.ComponentFactory, the jdk.jsobject module is gone, and
java.awt.peer.WindowPeer gained getAppropriateGraphicsConfiguration().
Nothing to configure: the right variant is chosen per application JVM — the launcher puts
exactly one of suramadu-app-toolkit-java11 / suramadu-app-toolkit-java27 on the boot
classpath, and the patched sun.awt.PlatformGraphicsInfo selects by Runtime.version().
A JDK 27 session announces itself in the application log:
Loaded Webtoolkit org.webswing.toolkit.WebToolkit27
Toolkit.toolkit field has been set to: org.webswing.toolkit.WebToolkit27@5a8e6209
GraphicsEnvironment is org.webswing.toolkit.ge.WebGraphicsEnvironment27 (headless, JDK patch active)
On JDK 27 the launcher additionally passes --enable-final-field-mutation=ALL-UNNAMED
(JEP 500) and serves netscape.javascript.JSObject from
the boot classpath instead of patching the removed jdk.jsobject module. Server and
application JVMs are independent — the Jetty server itself also runs on JDK 17–27.
Beware: JetBrains Runtime modifies AWT's internal keyboard focus dispatch, which breaks when the toolkit is replaced. Mouse events are unaffected because they bypass the patched focus path.
Try the Online JSQLFormatter running on Suramadu 
- Truly headless operation on Linux — no X server, no Xvfb, no virtual framebuffer. AWT initialises against an
--patch-module–injectedGraphicsEnvironment+FontManagerFactorythat bypasslibawt_xawt.soentirely. A*-jre-headlessJDK package is sufficient; container images shrink accordingly. - Shiro Security Plugin with hot configuration reloading provides authorization via AD/LDAP/Database/Text Files with or without 2FA (Second Factor authorization)
- SVG Application Icons
- better server start scripts supporting SSL certificate registration and WAR version numbers
- Deduplication of the java libraries in the WAR file, reducing the size drastically
- Network resilience for flaky corporate VPNs (Netskope, Zscaler) — HTTP/2 via ALPN, Conscrypt TLS (tolerates underscored SNI hostnames), per-socket TCP keepalive, 5-minute idle timeouts, TLS session resumption, tuned thread pool with
LowResourceMonitor
- Node.js 24 LTS — migrated from Node 10; Webpack 5, TypeScript 5, Dart Sass
- Gradle build system — fast, incremental builds
- All dependencies updated — Jetty 12, Jackson 3, Guava, Log4j2, SLF4J 2.0, Apache Commons, Protocol Buffers, LZ4, and more
- SSE/AVX-optimized PNG encoding via zpng-java — hardware-accelerated image compression in the DirectDraw rendering pipeline
- Browser-side font rendering — text is rendered as font names + coordinates instead of server-side glyph bitmaps, reducing WebSocket bandwidth by up to 80%
- GZIP/Brotli pre-compression for all static assets (reducing the JS size from 5 MB to less than 1 MB), GZIP compression for all content (JSON)
- Faster text drawing (27.1) — runs of characters are drawn with one
fillTextcall instead of one per character, with cached text measurements; per-character fallback only where browser and server metrics disagree. Text-heavy redraws render about twice as fast (benchmark: 17 → 9 ms median, 95th percentile 30 → 13 ms) - Faster image decoding (27.1) — images are decoded with
createImageBitmapstraight from the binary data, off the main thread, instead of building base64 data URLs - Less work on the Swing event thread (27.1) — PNG encoding of images happens lazily on the sender thread instead of while painting; image identity is hashed directly from the raster; fewer allocations per encoded frame
- Built-in DirectDraw benchmark (27.1) — a self-driving Swing application (
/benchmark: table scrolling, icons, vector graphics, image flood), per-frame server and client timings written to CSV (-Dwebswing.benchmarkStatsFile), an analyzer and a reproducible browser launcher. See the benchmark README - Experimental Web Worker rendering (27.1, off by default) — see the dedicated section for measurements and why it stays disabled
The fork is hardened for the deployments it was built for.
Continuous monitoring via Snyk (dependency CVEs), Semgrep (SAST/OWASP Top 10), SpotBugs (bytecode analysis), and Codacy (code quality). CycloneDX SBOM generated with every release.
Proactive hardening against entire vulnerability classes — not just known CVEs: deserialization allowlists (CWE-502), SSRF scheme validation (CWE-918), XSS content-type enforcement (CWE-79), Zip Slip / path traversal protection (CWE-22), log injection sanitisation (CWE-117), AES-CBC→AES-GCM migration, and HMAC-signed file identifiers.
Runtime — non-root Docker container on Eclipse Temurin JRE 21 (Ubuntu Noble), multi-stage build excluding source and build tools from the image.
Supply chain — CycloneDX SBOM output supports transparency requirements under NIST SP 800-218 and the EU Cyber Resilience Act.
| Component | Version |
|---|---|
| JDK | 17 or later, up to and including JDK 27 (21+ recommended; Eclipse Temurin). A headless JDK distribution is sufficient on Linux servers — see below. |
No X server, no Xvfb, no virtual framebuffer required. Suramadu runs truly headless on Linux via --patch-module java.desktop replacements for GraphicsEnvironment, PlatformGraphicsInfo, and FontManagerFactory. The standard *-jre-headless package from your distribution (or any JRE/JDK without the GUI dependencies) is enough. DISPLAY is explicitly unset by the startup script; nothing in the child Swing JVM ever calls into libawt_xawt.so or attempts a display connection.
# Download the latest release
curl -LO https://github.com/manticore-projects/suramadu/releases/latest/download/suramadu-27.2.0.zip
unzip suramadu-27.2.0.zip
cd suramadu-27.2.0
# Start the server
./run.sh start
# Open in your browser
open http://localhost:8080./run.sh start # Start the server (background, with log tailing)
./run.sh stop # Graceful shutdown (30s timeout, then SIGKILL)
./run.sh restart # Stop + Start
./run.sh status # Check if the server is running| Tool | Version |
|---|---|
| JDK | 17+ to run the build, plus an installed JDK 27 — the suramadu-app-toolkit-java27 module compiles with a JDK 27 toolchain (auto-detected from installed JDKs) |
| Gradle | 9.+ |
| Git | 2.x |
Node.js and npm are automatically downloaded during the build — no manual installation needed.
# Clone the repository
git clone https://github.com/manticore-projects/suramadu.git
cd suramadu
# Full build
./gradlew clean build
# Build specific modules
./gradlew :suramadu-directdraw:suramadu-directdraw-javascript:build
./gradlew :suramadu-server:suramadu-server-frontend:build# Extract to your deployment directory
unzip build/dist/suramadu-27.2.0.zip -d /opt/suramadu
# Configure your Swing application
vim /opt/suramadu/suramadu.config
# Start
cd /opt/suramadu && ./run.sh startEverything a user or administrator touches now carries the suramadu name. Everything that would
break a running deployment — system properties, Java packages, the public API — is unchanged.
| Before | Now | |
|---|---|---|
| Distribution archive | webswing-<version>.zip |
suramadu-<version>.zip |
| Web application | webswing-server-<version>.war |
suramadu-server-<version>.war |
| Application configuration | webswing.config |
suramadu.config |
| Server properties | webswing.properties |
suramadu.properties |
| Gradle modules and jars | webswing-* |
suramadu-* |
| Docker image | — | manticore-projects/suramadu |
| System properties | -Dwebswing.* |
unchanged |
| Jetty properties keys | org.webswing.server.* |
unchanged |
| Java packages and API | org.webswing.* |
unchanged |
No action is required to upgrade. Existing webswing.config and webswing.properties files are
still read, so an installation created before the rename keeps working untouched. Rename them when
convenient, or leave them. Where both names are present, the suramadu.* files take precedence.
Java packages keep the org.webswing namespace deliberately: renaming them would break every
deployment that references a class by name, and would require altering the copyright and licence
notices the AGPL obliges us to preserve. They are internal identifiers, not product names.
How it works: The server intercepts Java2D Graphics2D paint operations in the child JVM, serializes them via Protocol Buffers, and streams them over WebSocket to the browser. The browser's JavaScript engine deserializes and replays the draw commands on an HTML5 Canvas. User input (mouse, keyboard) flows back over the same WebSocket.
| Component | Version | Purpose |
|---|---|---|
| Jetty | 12.1.+ | Embedded HTTP/WebSocket server |
| Jackson | 3.+ | JSON serialization |
| Protocol Buffers | 3.+ | Binary wire format (DirectDraw) |
| Apache Shiro | 3.+ | Authentication & authorization |
| Guava | 33.+ | Core utilities |
| Log4j 2 | 2.+ | Logging framework |
| SLF4J | 2.+ | Logging facade |
| Webpack | 5.+ | JavaScript bundling |
| TypeScript | 5.+ | Type-safe frontend code |
Suramadu is derived from WebSwing v20.2.5, the last release published under the GNU AGPL v3. All credit for the original architecture and implementation belongs to its authors.
Suramadu is not affiliated with, endorsed by, or supported by Webswing Limited or Webswing s.r.o.
This project is licensed under the GNU Affero General Public License v3.0.
Suramadu contains an optional render path that moves DirectDraw decoding and canvas replay off
the browser's main thread into a dedicated Web Worker with an OffscreenCanvas. It is
disabled by default and should stay that way until the findings below change. This section
records what was measured, what was ruled out, and why it is switched off, so the next attempt
does not start from zero.
The worker owns the DirectDraw interpreter, the cumulative constant pool and one OffscreenCanvas
per Swing window. Each frame is replayed in the worker and handed back as a transferred
ImageBitmap, which the main thread blits into the regular window canvas. Everything that reads
window pixels on the main thread (dirty-rect blits, moveAction, getPixels, undocking, mirror
view) is unaffected.
| Enable | append ?ddWorker=true to the application URL |
| Benchmark | bench-browser.fish chromium|firefox 1600 1000 worker |
| Code | suramadu-dd-worker.ts, suramadu-dd-client.ts, routing in suramadu-base.ts (initDirectDraw, renderInWorker) |
| Fallback | any worker failure switches the session to main-thread rendering |
Implementation constraints that took effort to find and must be kept:
- One constant pool. The server sends every draw constant exactly once. All DirectDraw frames, base64 and binary alike, must go to the same interpreter; splitting them between main thread and worker corrupts rendering.
- Copy frame bytes at call time.
win.directDrawis a view into the WebSocket buffer; a deferred copy can capture a later frame. - Serial rendering. The interpreter is not re-entrant; the worker processes one frame at a time.
- Start the worker from a Blob URL. The page CSP allows
worker-src blob:only.
Benchmark application /benchmark, same window and geometry for every run (controls VECTOR and
IMAGE_FLOOD identical in bytes per frame):
| Chromium main thread | Chromium worker | Firefox 158 main thread | Firefox 158 worker | |
|---|---|---|---|---|
| TEXT_TABLE fps | 18.5 | 19.5 | 27.2 | 0.4 (collapse) |
| TEXT render p50 per KB | 0.53 ms | 0.54 ms | 0.25 ms | n/a |
| ICON_TABLE fps | 27.3 | 26.8 | 27.8 | 0.3 (collapse) |
| ICON render p95 | 23 ms | 31 ms | 14 ms | n/a |
Firefox worker figures were measured before the ack-timeout fix below; with the fix the session no longer collapses completely, but it still stalls repeatedly and stays far behind the main thread.
- Chromium: no measurable gain. The work moves to another thread but costs the same per KB; tails are equal or slightly worse (extra bitmap round trip).
- Firefox: the worker is CPU-bound. A profile showed the worker thread 97.8 % busy in
software rasterization (
fill,fillText,drawImage, plus largememcpy), while Firefox's accelerated canvas thread (CanvasRenderer) sat at 0.6 %. On the main thread Firefox uses accelerated Canvas2D and is the fastest browser; inside the worker the same canvas work runs in software, several times slower.
about:support reports Accelerated Canvas2D: available and gfx.canvas.remote.allow-offscreen
is true, yet the worker's canvas was not accelerated. Ruled out by experiment, each without
effect:
gfx.canvas.accelerated.min-size = 1(our canvases are created at 1×1 before resizing)gfx.canvas.accelerated.profile-fallback-ratio = 1.0andprofile-cache-miss-ratio = 1.0(Firefox's per-canvas demotion heuristic)- font loading in the worker (0.6 % of worker time in the profile)
Why Firefox renders a worker OffscreenCanvas in software in this configuration is not
known. The machine has no Firefox GPU process (FEATURE_FAILURE_NO_GPU_PROCESS), which may be
related; no documentation was found that confirms it.
- No gain in Chromium.
- In Firefox it trades GPU rendering for software rendering: strictly worse.
- The only remaining argument for it, a free main thread for input during heavy redraws, has not been measured; the benchmark scrolls server-side without user input.
The worker made the client slower, and that exposed three server-side bugs that also hit real users on slow connections:
- Constant-id recycling inside a frame (
LRUDrawConstantPoolCache,DrawConstantPool,WebImage): nestedDRAW_WEBIMAGEencodes reset the per-frame overflow guard, so large accumulated frames recycled constant ids mid-frame. The browser applies a frame's outer constants first and nested ones lazily, so ids resolved to the wrong constant: the UI corruption seen on slow connections. Fixed with a per-frame stamp on cache entries: an entry used in the current frame is never evicted; the cache grows instead. - Ack-timeout cache reset (
AbstractPaintDispatcher): after 5 s without a paint ack the server reset its constant cache and re-sent everything (fonts included, frames of several MB) to the client that was already too slow, a feedback loop that collapsed slow sessions to under 1 fps. The timeout now re-enables sending without a reset. - Cache reset race (
AbstractPaintDispatcher): a clientrepaint, resize handshake or recording start reset the cache on the event thread, possibly in the middle of an encode on the sender thread. Resets are now deferred to the start of the next sender cycle.
Worth another look when a browser accelerates OffscreenCanvas in workers on the target
platform, or when input latency during heavy redraws becomes a measured problem. Verify first
with a Firefox Profiler capture including the DOM Worker threads: the CanvasRenderer thread
must be busy and the worker must not spend its time in raster code. Then compare
?ddWorker=true against the default with bench-browser.fish, alternating runs in the same
window, and accept the worker only if it wins in both browsers.
Maintained by Manticore Projects
Building enterprise financial software for banks and insurances since 2011.