Back to articles

Real-Time Vue Apps, Part 2: WebSockets and Real-Time Databases

Vue
Real-Time Vue Apps, Part 2: WebSockets and Real-Time Databases

Hello there! This article is adapted from my talk at MadVue 2026, the amazing Vue conference that took place in Madrid, Spain. Make sure to check out this page for the slides, all the articles and some pics!

Let's continue exploring how can we implement real-time features in our Vue and Nuxt apps. In Part 1, we discovered that polling is not great and that SSE (Server-Sent Events) are a great alternative when you can just use a classic HTTP connection.

WebSockets

But there's another option for real-time apps, perhaps the most popular one: WebSockets. They allow not only sending data from server to client, but sending data from client to server as well. They are a bi-directional way, and the ideal solution for many apps in need of real-time features:

  • Chats and chatbots
  • Collaborative apps (like Notion, Figma and more)
  • Multiplayer games

... and any other scenario where you need a fast way of sending and receiving data from the server.

In this article we'll explore how they work, how can we implement them using Vue and Nuxt, and how they server other solutions like real-time databases. Ready? Let's go!

How Do WebSockets Work?

A WebSocket connection starts as a regular HTTP request. The client sends a special header, Upgrade: websocket, basically asking the server "hey, can we upgrade this connection to a socket?". It also sends a random Sec-WebSocket-Key along with it.

If the server supports websockets and accepts, it hashes that key with a magic GUID and returns the result in a Sec-WebSocket-Accept header, alongside a 101 Switching Protocols response. This little crypto dance proves the server actually understood the request. You don't need to worry about it, the browser and the server handle it for you.

After that, the protocol changes from http:// to ws:// (or wss:// for secure).

Once that's done, the connection is established and both sides can send messages whenever they want.

Implementing WebSockets with Vue & Nuxt

Many applications use Pusher, Socket.IO, or similar services to support WebSockets. Many others use a dedicated, self-hosted solution like Reverb or WS.

The cool thing is that, if you're using Nuxt, you don't need a special service, perhaps not event a dedicated server: WebSockets are built into Nuxt thanks to Nitro.

Your Nuxt application can handle them, out of the box. Let's check that out!

Server-Side

In a server route, you define a special handler with defineWebSocketHandler. This handler gives you access to a set of hooks:

export default defineWebSocketHandler({
  // Runs when a peer connects
  open(peer) {
    // Subscribe the peer to a topic
    peer.subscribe('room-123')
  },
  // Runs when a peer sends a message
  message(peer, message) {
    // Broadcast the message to everyone else
    peer.publish('room-123', message.text())
  }
  // Other hooks: upgrade(), close(), error()
})
  • The open hook runs whenever a peer connects. In this case, we're subscribing the peer to a topic: the ID of a chat room, 'room-123'. It can be whatever string you want.
  • The message hook runs whenever a peer sends a message. Here, we're publishing the text of that message to everyone subscribed to that topic.

You have other hooks too, like upgrade, close, and error. You can explore them in the official documentation.

Let's check out the methods of the peer object:

// Subscribes the peer to a topic
peer.subscribe('room-123')

// Sends a message to everyone in the topic (the sender excluded)
peer.publish('room-123', 'Hello there!')
peer.publish('room-123', { user: 'Alice', text: 'Hi everyone!' })

// Sends something to that peer only.
// Great for messages like "Welcome to the chat room", notifications, etc.
peer.send('Welcome!')
peer.send({ type: 'notification', text: 'You are awesome' })

// Stops listening to the topic
peer.unsubscribe('room-123')

// Gracefully closes the connection
peer.close()

Client-Side

On the client side, VueUse has us covered again with useWebSocket.

Take a look at this code. It's a minimal chat component:

<script setup>
import { useWebSocket } from '@vueuse/core'

const messages = ref([])
const { send } = useWebSocket('ws://localhost:3000/_ws', {
  onMessage: (ws, e) => messages.value.push(e.data)
})

const input = ref('')
function submit() {
  send(input.value)
  messages.value.push(`Me: ${input.value}`)
  input.value = ''
}
</script>

<template>
  <div v-for="(msg, i) in messages" :key="i">{{ msg }}</div>
  <input v-model="input" @keyup.enter="submit" />
</template>

Let's explore what we have. We're connecting to the WebSocket, and we say: every time you get a message, push it into the messages array. We also destructure the send method, which we use below: whenever the user types something into the input and presses enter, we send the message via the socket and push it into the array of messages.

In just about 20 lines of code, we have a working chat. Of course, in a real-world application you'd need many more features, but this shows how simple it is to work with WebSockets in Vue.

Two More Things: Auto-Reconnect and Heartbeat

Before moving on, two cool things about WebSockets that you'll want to keep in mind.

Auto-reconnect is pretty similar to SSE. The only difference is that with useWebSocket you have to opt in, it's not on by default:

// Same idea as SSE, but with WebSockets you have to opt in
useWebSocket('ws://localhost:3000/_ws', {
  autoReconnect: {
    retries: 5,
    delay: 2000,
    onFailed: () => console.log('Could not reconnect'),
  }
})

And then there's the heartbeat. WebSockets stay open forever, in theory, but proxies and load balancers love to kill idle connections. So when nothing is being exchanged between server and client, it's recommended to regularly send a ping to the server and receive a pong back, just to keep the connection alive.

// Proxies and load balancers love to kill idle connections,
// send a ping every 30 seconds to keep them alive
useWebSocket('ws://localhost:3000/_ws', {
  heartbeat: {
    message: 'ping',
    interval: 30000,
    pongTimeout: 1000,
  }
})

If the server doesn't pong back within pongTimeout, the composable considers the connection dead and (if you opted in) reconnects.

Real-Time Databases

This is a great moment to talk about real-time databases, because WebSockets are exactly what they use under the hood to broadcast changes.

What's the idea? In your client-side code, you subscribe to listen for changes on certain collections (in NoSQL databases) or tables (in relational ones). If a record is created, updated, or deleted, every connected client gets the details in real time.

You don't need to worry about wiring up the socket: the libraries handles that for you. You just pick the changes you want to listen for, and handle what to do with them by defining a callback.

For example, let's say you have a list of tasks, and whenever those tasks change you want this template to automatically re-render:

<template>
  <ul v-if="tasks">
    <li v-for="task in tasks" :key="task.id">
      {{ task.user }}: {{ task.text }}
    </li>
  </ul>
  <p v-else>Loading...</p>
</template>

How can we keep tasks up to date in real time? Let's look at two great options.

Firebase and VueFire

Firebase comes in two flavors, the Realtime Database and Firestore, both NoSQL, both real-time. The best way to use it in Vue is with VueFire, the amazing library by Eduardo San Martín Morote.

// VueFire 🔥
import { useCollection } from 'vuefire'
import { collection } from 'firebase/firestore'
import { db } from '~/firebase'

const tasks = useCollection(collection(db, 'tasks'))

That's it. Super short. tasks is a reactive ref, so whenever the tasks collection changes, the template re-renders.

Supabase and the Nuxt module

If you want an open-source alternative, there's Supabase. It's a Postgres database with real-time updates you can subscribe to. If you're using Nuxt, I recommend the excellent Nuxt Supabase module.

// Nuxt Supabase ⚡️
const client = useSupabaseClient()

const { data: tasks, refresh } = await useAsyncData('tasks', async () =>
  (await client.from('tasks').select('*')).data
)

let channel

onMounted(() =>
  channel = client
    .channel('public:tasks')
    .on('postgres_changes', {
      event: '*', schema: 'public', table: 'tasks'
    }, refresh)
    .subscribe()
)

onUnmounted(() => client.removeChannel(channel))

Same idea: you write to your database, and every subscribed client gets the change in real time.

WebSockets Are Great! But...

WebSockets are great, and real-time databases make them even more convenient. But there's a catch: you depend on a server.

What if you don't want a server in the middle? What if you could connect directly to your peer, browser to browser?

Well, that's possible thanks to WebRTC, and that's exactly what we'll explore in Part 3: WebRTC and PeerJS.

See you there!

More in Vue