Explore
Building Real-Time Features in a Nuxt App

Building Real-Time Features in a Nuxt App

Real-time updates transform collaborative products. Here are the implementation options in Nuxt — when to use each, and how to build them without over-engineering.

Building Real-Time Features in a Nuxt App

Real-time updates — notifications that appear without refreshing, collaborative cursors, live activity feeds — are the features that make products feel alive. They're also where a lot of startups over-engineer early and under-engineer when it actually matters.

Here's the practical breakdown of how to implement real-time features in a Nuxt application.


Do you actually need real-time?

Before the implementation, ask: does this feature require real-time, or just near-real-time?

Requires actual real-time (<500ms):

  • Collaborative editing (Google Docs style)
  • Multiplayer cursors
  • Live auction bids
  • Trading/pricing data

Near-real-time is sufficient (1-30 seconds):

  • Notification counts
  • Activity feeds
  • Comment threads
  • Background job status

The difference matters because the implementation complexity is very different.


Option 1: Polling (simple, often enough)

Before reaching for WebSockets, consider polling. An API call every 10-30 seconds catches updates that are near-real-time with no additional infrastructure.

// composables/usePolling.ts
export function usePolling(fn: () => Promise<void>, intervalMs: number) {
  let interval: NodeJS.Timeout | null = null

  function start() {
    fn() // Run immediately
    interval = setInterval(fn, intervalMs)
  }

  function stop() {
    if (interval) clearInterval(interval)
  }

  onMounted(start)
  onUnmounted(stop)
}
<script setup>
const { data: notifications } = useFetch('/api/notifications')
usePolling(() => refreshNuxtData('notifications'), 30_000)
</script>

When polling is fine: Notification counts, activity feeds, dashboard metrics where a 30-second delay is acceptable.

When polling isn't fine: Collaborative features where you can't tolerate lag, or high-frequency updates where polling would generate too many requests.


Option 2: Server-Sent Events (one-way, simple)

SSE is a native web feature: the server sends a stream of events over a persistent HTTP connection. It's simpler than WebSockets because it's one-directional (server → client).

// server/api/events.get.ts
export default defineEventHandler((event) => {
  setHeader(event, 'Content-Type', 'text/event-stream')
  setHeader(event, 'Cache-Control', 'no-cache')
  setHeader(event, 'Connection', 'keep-alive')

  // Send an event every time something changes in your data layer
  // This is simplified — in practice you'd subscribe to a pub/sub channel
  const interval = setInterval(() => {
    event.node.res.write(`data: ${JSON.stringify({ type: 'ping' })}\n\n`)
  }, 15000)

  event.node.req.on('close', () => {
    clearInterval(interval)
  })
})
// composables/useSSE.ts
export function useSSE(url: string) {
  const eventSource = ref<EventSource | null>(null)

  function connect() {
    eventSource.value = new EventSource(url)
    return eventSource.value
  }

  onUnmounted(() => {
    eventSource.value?.close()
  })

  return { connect }
}

When SSE works well: Live notifications, status updates, one-way data streams.


If you're already using Supabase as your database, Supabase Realtime is by far the simplest path to real-time features. It gives you WebSocket-based subscriptions to database changes with no additional infrastructure.

// composables/useRealtimeComments.ts
export function useRealtimeComments(postId: string) {
  const supabase = useSupabaseClient()
  const comments = ref<Comment[]>([])

  async function fetchComments() {
    const { data } = await supabase
      .from('comments')
      .select('*')
      .eq('post_id', postId)
      .order('created_at', { ascending: true })
    comments.value = data ?? []
  }

  onMounted(() => {
    fetchComments()

    supabase
      .channel(`comments:${postId}`)
      .on('postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'comments', filter: `post_id=eq.${postId}` },
        (payload) => {
          comments.value.push(payload.new as Comment)
        }
      )
      .subscribe()
  })

  return { comments }
}

This pattern:

  • Fetches existing data on mount
  • Subscribes to database changes
  • Appends new comments as they arrive — no polling, no manual refresh

Use Supabase Realtime when: You're on Supabase and need real-time for relatively standard data patterns (new rows, updated fields, deleted records).


Option 4: WebSockets for custom real-time logic

When you need bidirectional real-time communication and Supabase Realtime doesn't fit, WebSockets are the right tool. Nuxt handles WebSocket upgrades natively through Nitro.

This is the right choice for:

  • Collaborative editing
  • Multiplayer games
  • Custom pub/sub patterns
  • High-frequency message passing

For most startups, this level of custom real-time infrastructure is premature. Validate the need first.


Handling reconnections

Any real-time connection should handle disconnections gracefully:

export function useSSEWithReconnect(url: string) {
  const source = ref<EventSource | null>(null)
  let retryCount = 0

  function connect() {
    source.value = new EventSource(url)

    source.value.onerror = () => {
      source.value?.close()
      // Exponential backoff: 1s, 2s, 4s, 8s, max 30s
      const delay = Math.min(1000 * 2 ** retryCount, 30000)
      retryCount++
      setTimeout(connect, delay)
    }

    source.value.onopen = () => {
      retryCount = 0 // Reset on successful connection
    }
  }

  onMounted(connect)
  onUnmounted(() => source.value?.close())

  return source
}

The pragmatic path

For most startup products:

  1. Start with polling for features where a 30-second delay is acceptable
  2. Add Supabase Realtime when you need true real-time updates and you're on Supabase
  3. Use SSE for server-push scenarios without bidirectional needs
  4. Reach for WebSockets only when you have genuinely custom real-time logic

Don't over-engineer real-time infrastructure early. Get the feature working, validate that users care about the real-time aspect, then invest in the right implementation.

Building real-time features in your product? Let's talk →