nuxt-realtime
nuxt-realtime

Real-time state synchronization for Nuxt applications. Share reactive state across clients, broadcast events, and manage connections.

Nuxt Realtime

npm versionnpm downloadsLicenseNuxt

Drop-in realtime state sync for Nuxt. Implement useRealtimeState() and it works across all connected clients.

Features

  • 🔄  Real-time state synchronization across all connected clients
  • 📡  Event pub/sub with wildcard channel subscriptions
  • 🎯  Three sync strategies: immediate, debounced, and manual
  • 🔌  Built on Socket.IO for reliable WebSocket connections
  • 🎨  Works just like useState
  • 🔐  Auth hooks via a Nitro plugin
  • 🏗️  Redis adapter for cross-server state sync in multi-instance deployments
  • 🪶  Zero boilerplate

Quick Setup

Full documentation

Install the module to your Nuxt application with one command:

npx nuxi module add nuxt-realtime

Then enable WebSockets in your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-realtime'],
  nitro: {
    experimental: { websocket: true },
  },
})

Usage

State sync

useRealtimeState works like useState but syncs across every connected client:

<script setup>
const counter = useRealtimeState('counter', 0)
</script>

<template>
  <div>
    <p>Count: {{ counter }}</p>
    <button @click="counter++">Increment</button>
  </div>
</template>

Every assignment to counter.value (or counter++ in templates) is immediately broadcast to all other clients subscribed to the same key.

Sync strategies

Control when local changes are pushed to the server:

<script setup>
// immediate  syncs on every change
const liveText = useRealtimeState('text:live', '')

// debounced buffers rapid changes, syncs after 500 ms of inactivity
const debouncedText = useRealtimeState('text:debounced', '', {
  sync: 'debounced',
  debounceMs: 500,
})

// manual local-only until sync() is called explicitly
const draftText = useRealtimeState('text:draft', '', { sync: 'manual' })
const isDirty = draftText.isDirty
const save = draftText.sync
</script>

<template>
  <input v-model="debouncedText" />

  <div>
    <input v-model="draftText" />
    <button :disabled="!isDirty" @click="save">Save</button>
  </div>
</template>

useRealtimeState also exposes loading (true while the initial value is being fetched from the server) and refresh() to manually re-fetch.

Event pub/sub

useRealtimeEvents is a lightweight pub/sub layer on top of the WebSocket connection. Use it for one-way broadcasts that don't need shared state like notifications, typing indicators, page events etc.

<script setup lang="ts">
interface ChatEvents {
  'chat:message': { userId: string; text: string }
  'chat:typing': { userId: string }
}

const { publish, subscribe } = useRealtimeEvents<ChatEvents>()

// Subscribe to a specific channel
subscribe('chat:message', (msg) => {
  console.log(msg.userId, msg.text)  // fully typed
})

// Wildcard channel fires for chat:message, chat:typing, chat:anything
subscribe('chat:*', (data, channel) => {
  console.log('received on', channel, data)
})

async function send(text: string) {
  await publish('chat:message', { userId: 'me', text })
}
</script>

Subscriptions are cleaned up automatically when the component unmounts.

You can also add middleware to intercept or transform events before subscribers receive them:

const { subscribe } = useRealtimeEvents({
  middleware: [
    (event, next) => {
      if (!isValid(event.data)) return  // block the event
      event.data = sanitize(event.data) // mutate the payload
      next()
    },
  ],
})

Connection status

<script setup>
const { status, connected, connect, disconnect } = useRealtimeConnection({
  onReconnecting: (attempt) => console.log('reconnecting, attempt', attempt),
  onReconnected: () => console.log('back online'),
})
</script>

<template>
  <span>{{ status }}</span>  <!-- 'connected' | 'disconnected' | 'connecting' | 'reconnecting' -->
</template>

Configuration

All options go under nuxtRealtime in your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-realtime'],
  nuxtRealtime: {
    // Redis for cross-server state sync (multi-instance deployments)
    redis: {
      host: 'localhost',
      port: 6379,
      // or: url: 'redis://localhost:6379'
    },

    // Custom Socket.IO server URL (defaults to same origin)
    socketio: {
      serverUrl: 'https://my-realtime-server.com',
      path: '/socket.io',
    },

    // Idle-key cleanup (defaults shown; set to false to disable)
    cleanup: {
      heartbeatInterval: 30_000,   // ms between client heartbeats
      cleanupInterval: 300_000,    // ms between server scans
      idleThreshold: 3_600_000,    // ms before an idle key is removed
    },

    // Logging
    logging: {
      level: 'warn',   // 'debug' | 'info' | 'warn' | 'error' | 'silent'
      format: 'json',  // 'pretty' (default) | 'json'
    },
  },
})

Auth middleware

Register Socket.IO middleware in a Nitro server plugin to authenticate or authorize clients before any connection handler runs:

// server/plugins/realtime-auth.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('nuxt-realtime:io', (io) => {
    io.use((socket, next) => {
      const token = socket.handshake.auth.token
      if (!verifyToken(token)) return next(new Error('Unauthorized'))
      next()
    })
  })
})

Multi-server / Redis

When running multiple server instances, add the redis option so that state writes and event broadcasts reach clients connected to any instance:

nuxtRealtime: {
  redis: {
    host: process.env.REDIS_HOST,
    port: Number(process.env.REDIS_PORT),
  },
},

ioredis >= 5 must be installed as a peer dependency.

Contribution

We welcome contributions! See CONTRIBUTING.md for setup and commit message guidelines.