Creating a Grails application with a WebSocket plugin

Real-time features have moved from being a novelty to a baseline expectation in modern web software. Whether it is a live chat overlay for a customer support portal in Melbourne, a sports ticker for an AFL fan app, or a collaborative dashboard shared between analysts in Sydney and Perth, users want updates pushed to their browsers without pressing refresh. Grails, built on Groovy and Spring Boot, makes it surprisingly approachable to add this kind of behaviour, especially when you bring in a plugin that handles the heavy lifting around WebSockets.

The aim of this walkthrough is to take you from an empty directory to a running Grails application where a browser and a server exchange messages freely over a persistent connection. Along the way we will look at project scaffolding, plugin configuration, server-side broadcasting, a thin JavaScript client, and the practical security and hosting concerns that come up when you ship something like this from Australia to a real audience.

Setting up the Grails project

Before writing any code you need a working Grails CLI on your machine. The framework supports Java 11 and newer, so any modern JDK will do. Many Australian developers working in the financial and logistics sectors favour Adoptium Temurin builds because the licensing terms are unambiguous for commercial use, and you will see those downloads peak every time Atlassian pushes a new IntelliJ IDEA release. Once Java is on your PATH, install Grails with SDKMAN, which is the most frictionless option on macOS, Linux and Windows Subsystem for Linux.

From there, scaffold a new application with the create-app command:

grails create-app realtime-demo
cd realtime-demo

Grails 5 and 6 both follow the same conventions, so the rest of this guide works regardless of which major version you pick. Open the project in your IDE of choice and confirm that the default controller renders on http://localhost:8080. A quick sanity check now saves headaches later, especially if you are behind a corporate proxy common in Brisbane and Canberra government offices.

If you have never built a Grails app before, it helps to understand how the framework layers itself on top of Spring Boot. Every controller, service and domain class you create lives inside a familiar MVC structure, and Spring auto-configuration takes care of HTTP plumbing. That foundation is what makes adding a WebSocket plugin almost a drop-in affair, since WebSockets are simply another endpoint inside the embedded Tomcat server that Grails already manages. Familiarity with GORM and the GSP view layer also pays off, because the front-end page that hosts the live connection is just another GSP template with a small JavaScript snippet.

Adding the WebSocket plugin

There are a few options for WebSockets in the Grails plugin ecosystem, but the most actively maintained one is the grails-spring-websocket-support plugin, which exposes a clean annotation-driven programming model on top of Spring's STOMP-over-WebSocket implementation. If your team is more comfortable with raw sockets, the standalone WebSocket plugin offers a lower level API. Either choice plugs in through Gradle, and you switch between them without rewriting your application logic.

Open build.gradle and add the plugin to the dependencies block. For Spring's STOMP integration:

dependencies {
    implementation 'org.grails.plugins:grails-spring-websocket-support:3.0.0'
}

For the lighter raw-socket plugin:

dependencies {
    implementation 'org.grails.plugins:grails-websocket:3.0.0'
}

Run a ./gradlew dependencies refresh so your IDE indexes the new jars, then decide which configuration style you prefer. STOMP brings topic-based pub/sub and works beautifully with front-end libraries like StompJS, while raw WebSockets keep your message envelope entirely in your hands. For a chat-style application that may grow into a dozen event types, STOMP pays off quickly. For a single fire-and-forget notification stream, raw sockets stay closer to the wire and easier to debug.

The plugin also needs a small amount of wiring in grails-app/conf/application.yml. For the STOMP plugin, point Spring at your broker and enable the simple in-memory relay:

grails:
    plugin:
        springwebsocket:
            simple:
                broker:
                    enabled: true
            handler:
                allowed-origins:
                    - 'https://your-domain.com.au'

This allow-list pattern matters more than people expect, because the WebSocket handshake does not go through CORS in the usual sense. An attacker who can guess your endpoint URL can otherwise open thousands of connections from a script. The list above is the right place to draw the line, and it pairs naturally with a content security policy that lists the same origins.

Building the WebSocket handler and controller

With the plugin on the classpath, you can now define a controller that opens a WebSocket endpoint and a service that pushes events into it. In the STOMP flavour, a typical broadcast looks like this:

@Controller
class StreamController {

    @MessageMapping('/updates')
    @SendTo('/topic/news')
    Map update(Map message) {
        return [payload: message.text, ts: System.currentTimeMillis()]
    }
}

The annotation pair wires a STOMP destination to a method and rebroadcasts the return value to subscribers of /topic/news. From a service you can also push messages proactively using SimpMessagingTemplate, which is useful for timers, database triggers or external Kafka consumers.

If you went the raw WebSocket route, the shape is similar but you manage the sessions yourself. A handler class receives open, message and close events, holds the session in a thread-safe set, and iterates over that set when something needs to go out. Be careful with serialisation here. Groovy makes it tempting to ship whole domain objects, but you should send small immutable maps or records instead so the wire format remains stable as your domain evolves.

Either way, the principle is the same. Keep the handler focused on transport and let a service own the business rules. If you have done any work with Grails async controllers, the separation of I/O concerns from application logic feels familiar, and the asynchronous controllers guide at async controllers tutorial explores similar territory for HTTP traffic.

Connecting the front end

The server side is only half of the story. Open grails-app/views/index.gsp or whichever page you want to host the live view, then drop in a small client. With STOMP and StompJS the snippet is short:

<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
<script>
  const client = new StompJs.Client({ brokerURL: '/ws/updates' });
  client.onConnect = () => {
    client.subscribe('/topic/news', (msg) => {
      document.getElementById('feed').innerText = msg.body;
    });
  };
  client.activate();
</script>

If you picked raw WebSockets, the equivalent is a one-liner using the browser's built-in API:

const socket = new WebSocket('ws://localhost:8080/updates');
socket.onmessage = (e) => updateFeed(e.data);

Test the round trip locally first. Open two browser windows, fire an event from a controller endpoint, and confirm that both windows receive it within a few hundred milliseconds. The lower your round-trip latency, the more confident you can be when you deploy to a region like AWS ap-southeast-2 in Sydney, where the typical cross-zone latency is well under ten milliseconds. Apache JMeter or the k6 load testing tool can simulate dozens of clients opening sockets at once, which is the easiest way to spot memory leaks in your handler.

For mobile or remote team members connecting from regional Western Australia or Tasmania, remember that edge caching and CDNs do not accelerate WebSocket traffic the way they accelerate static assets. Plan your back-end region accordingly so users in Hobart do not find themselves chatting with a server in Frankfurt.

Deploying and securing the application

Once the prototype works on your laptop, the next questions are about hosting, hardening, and the regulatory environment you operate in. Australian developers typically deploy Grails applications in one of three places: AWS Sydney, an Azure region in Australia East, or a local provider such as Servers Australia or Vultr's Melbourne point of presence. Each of these supports Java 17 or 21 runtimes and lets you point a Gradle-built JAR straight at a load balancer.

Security is where real care is required. WebSockets bypass many of the cookie-based protections that traditional REST endpoints enjoy, so you should require authentication on the upgrade request itself. The simplest pattern is to read the user's session at handshake time, reject anonymous connections with a 401, and only allow authenticated principals to subscribe to sensitive topics. If you store any personal information that flows through the socket, you also need to think about the Privacy Act 1988 and the Australian Privacy Principles, especially if your users are health, financial, or government-related.

The ACSC's Essential Eight maturity model is a sensible baseline for hardening the host. Disable directory listings, enforce HTTPS, run the application as a non-root user inside a container, and treat every WebSocket origin as untrusted until your server validates the Origin header against an allow-list. Terminate TLS at the load balancer and serve your pages under wss:// rather than ws://, so the upgrade handshake inherits the same encryption as the rest of your site. If you front the application with Cloudflare, double-check that its WebSocket support is enabled on the zone, because the toggle is off by default on free plans.

Logging is the last piece. Tomcat's access logs will record the upgrade but not the message bodies, which is exactly what you want from a privacy standpoint. Add structured application logs at the handler level so you can correlate frames with user sessions during incident response, and ship them to a centralised store like Elastic or Loki where you can keep them for the retention period your compliance team mandates. Health probes should also ping a dedicated /health endpoint that does not open a socket, so your orchestrator does not falsely mark the pod ready when the WebSocket layer is wedged.

A working real-time feature in a Grails application is satisfying to build and unlocks a great deal of product value, from live order tracking for an Adelaide e-commerce retailer to in-game chat for a Brisbane indie studio. If you get stuck, want to share what you have built, or have questions about scaling WebSockets across multiple application instances, the team behind Grails Example can be reached through the aboutcontact page. Pair the patterns above with the async controllers tutorial linked earlier and you will have a complete picture of how Grails handles both request-response and persistent streams without blocking a single thread.