Learn how to take the one-way WebSocket communication you saw in the Streamiverse example further to develop two-way WebSocket communication by building a simple echo server. It also introduces you to Kitten’s first-class support for Alpine.js.

Topics covered

WebSocket echos

In the Streamiverse example, we used a WebSocket to stream in post updates but we could just as well have used Server-Sent Events (SSE) given that the communication was one-way.

💡 Kitten does not support HTTP/2 so if you use SSE, people will be limited to 6 browser connections to your app. Given Kitten’s excellent support for WebSockets and how efficient WebSockets are, and since HTTP/2 support is not currently planned, neither is first-class support for Server-Sent Events.

What’s unique about WebSockets is that you can carry out asynchronous two-way communication.

So let’s take advantage of that by creating a very simple WebSocket echo server that simply returns what we send it.

First, let’s create the page:

export default () => kitten.html`
  <page htmx htmx-websocket water>

    <h1>WebSocket Echo</h1>
    <form id='message-form' ws-send>
      <label for='message'>Message</label>
      <input id='message' name='message' type='text' required> 
      <button type='submit'>Send</button>

    <ul id='echos' hx-swap-oob='beforeend'></ul>

The only new thing here is that we have a form with the ws-send

And now, let’s add the WebSocket route:


export default function ({ socket }) {'Echo socket: new client connection.', request.session)

  socket.addEventListener('message', event => {
    const message = JSON.parse(

      <ul id='echos' hx-swap-oob='beforeend'>
        <li><strong>“${message}”</strong> received on server.</li>

And that’s it. Now run the example and you should see your messages being echoed back to you.

Enhancing usability with Alpine.js

The most visible and annoying usability issue right now is that when you send a message, the message box is not cleared. So let’s fix that using Kitten’s built-in support for Alpine.js.

First, update the <page> tag so Kitten knows to include Alpine.js on the page:

<page htmx htmx-websocket water alpinejs>

Then, update the <input> element so it clears itself after the message has been sent:

<form id='message-form' ws-send><input 
    id='message' name='message' type='text' required
    @htmx:ws-after-send.window='$el.value = ""'

So what’s happening here is that we’re using Alpine’s @ syntax to specify an inline handler for htmx’s ws-after-send event.

💡 Remember that we can only use Alpine.js within Alpine.js components and the way you designate an Alpine.js component is to declare an x-data attribute on it. In this case, we have no actual data the component has to manage so we just use an empty x-data tag.

While htmx supports both camelCase and kebap-case for event names, Alpine.js can only work with kebap case since attribute names are case insensitive in HTML5. So while you will see events in the former style in the htmx documentation, remember to use kebap case when listening to htmx events using Alpine.js.

💡 The @ syntax for defining event handlers is a shortcut for Alpine’s more verbose x-on attribute so we could also have written that line as:

<inputx-on:htmx:ws-after-send.window='$el.value = ""'>

That said, given htmx uses a namespace (htmx:) that also contains a colon, you might find the shorthand @ syntax easier to author and read.

We also specify that we want to listen to the event on the window as it is dispatched from the parent <form> node so it would otherwise not reach the child <input> node (events in JavaScript bubble up, not down).

💡 To find out more about how Alpine.js works, make sure you read the Alpine.js documentation. It’s got a tiny API and you can likely work through the whole documentation in less than an hour.

Send a few more messages and notice that sent messages are now cleared from the input box.

Ah, that’s better! 😻

Now that we’ve seen how to implement two-way socket communication, let’s take everything we’ve learned so far and make our first peer-to-peer end-to-end encrypted Small Web app. We’ll start slowly by first building a simple ephemeral, centralised, and unauthenticated chat application and then iteratively enhance it to make it a persisted, decentralised (peer-to-peer), and end-to-end encrypted one.

Next tutorial: Streaming HTML