---
title: "View Active Connections"
description: "Monitor active connections and version distribution in real-time."
canonical_url: "https://nuxtseo.com/docs/skew-protection/guides/live-connections"
last_updated: "2026-05-09T10:32:35.689Z"
---

Track how many users are connected to your application and which versions they're running. This is useful for admin dashboards, deployment monitoring, and understanding rollout progress.

<callout type="warning">

Connection tracking only works with `sse` or `ws` update strategies. It does not support polling or external adapters (Pusher/Ably).

</callout>

## Setup

Enable connection tracking in your Nuxt config using [`connectionTracking`](/docs/skew-protection/api/config#connectiontracking-boolean):

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  skewProtection: {
    connectionTracking: true,
    routeTracking: true, // optional: track which routes users are viewing
    ipTracking: true, // optional: track user IP addresses
  }
})
```

### Limitations

- **Single instance only**: Stats are per-server process. With horizontal scaling, each instance only sees its own connections.
- **SSE/WS only**: Does not work with `polling` strategy or external adapters (Pusher/Ably).
- **Bot traffic excluded**: When you install `@nuxtjs/robots`, the module automatically excludes bot/crawler traffic from connection counts. See [Bot Traffic Filtering](/docs/skew-protection/guides/performance#bot-traffic-filtering).

### IP Tracking

IP tracking requires explicit opt-in via `ipTracking: true` due to privacy considerations. When enabled, the module extracts IP addresses from request headers in this order:

1. `cf-connecting-ip` (Cloudflare)
2. `x-forwarded-for` (proxies/load balancers)
3. `x-real-ip` (nginx)

<callout type="warning">

The module only stores IPs in memory and exposes them via the stats API. Ensure your authorization hook properly restricts access to stats data.

</callout>

## Authorization

The server only sends stats to connections that pass authorization. Implement the `skew:authorize-stats` hook in a server plugin.

```ts [server/plugins/skew-auth.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
    // Works with nuxt-auth-utils
    const session = await getUserSession(event)
    if (session?.user?.role === 'admin') {
      authorize()
    }
  })
})
```

### Cloudflare with nuxt-auth-utils

For [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) connections (e.g., with `cloudflare-durable` preset), the `event` object is not a full H3 event - it only contains `{ headers: Headers }`. This means `getUserSession()` won't work directly. You need to manually unseal the session cookie:

```ts [server/plugins/skew-auth.ts]
import { unsealSession } from 'h3'

const ADMIN_EMAILS = ['admin@example.com']

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
    // Try getUserSession first (works for SSE/POST)
    const session = await getUserSession(event).catch(() => null)
    if (session?.user?.email && ADMIN_EMAILS.includes(session.user.email)) {
      authorize()
      return
    }

    // For WebSocket, manually unseal the session cookie
    if (event?.headers instanceof Headers) {
      const cookieHeader = event.headers.get('cookie') || ''
      const match = cookieHeader.match(/nuxt-session=([^;]+)/)
      if (match?.[1]) {
        const config = useRuntimeConfig()
        const password = config.session?.password || process.env.NUXT_SESSION_PASSWORD || ''
        if (password) {
          const unsealed = await unsealSession(null as any, { password }, decodeURIComponent(match[1])).catch(() => null)
          // Note: unsealSession returns { data: { user } }, not { user } directly
          const email = unsealed?.data?.user?.email
          if (email && ADMIN_EMAILS.includes(email)) {
            authorize()
          }
        }
      }
    }
  })
})
```

<callout type="info">

The key difference: `getUserSession()` returns `{ user: { email } }` while `unsealSession()` returns `{ data: { user: { email } } }`.

</callout>

## Using the Composable

The [`useActiveConnections()`](/docs/skew-protection/api/use-active-connections) composable provides reactive access to connection statistics:

```vue
<script setup lang="ts">
import { useActiveConnections } from '#imports'

const { total, versions, authorized } = useActiveConnections()
</script>

<template>
  <div v-if="authorized">
    <p>Active users: {{ total }}</p>
    <ul>
      <li v-for="(count, version) in versions" :key="version">
        {{ version.slice(0, 8) }}: {{ count }} users
      </li>
    </ul>
  </div>
  <div v-else>
    Not authorized for stats
  </div>
</template>
```

### Return Values

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        total
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          number
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Total active connections
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        versions
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          Record<string,
        </span>
        
        <span class="sg-iE">
          number
        </span>
        
        <span class="sx-uw">
          >
        </span>
        
        <span class="sqjlB">
          >
        </span>
      </code>
    </td>
    
    <td>
      Map of buildId → connection count
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        routes
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          Record<string,
        </span>
        
        <span class="sg-iE">
          number
        </span>
        
        <span class="sx-uw">
          >
        </span>
        
        <span class="sqjlB">
          >
        </span>
      </code>
    </td>
    
    <td>
      Map of route → connection count (requires <code>
        routeTracking
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        connections
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          ConnectionInfo[]
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Individual connections with <code>
        id
      </code>
      
      , <code>
        version
      </code>
      
      , <code>
        route
      </code>
      
      , and <code>
        ip
      </code>
      
       (requires <code>
        ipTracking
      </code>
      
       for IP)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        yourId
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          string
        </span>
        
        <span class="sg-iE">
          |
        </span>
        
        <span class="sg-iE">
          undefined
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Your connection ID to identify yourself in the list
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        authorized
      </code>
    </td>
    
    <td>
      <code className="language-html shiki shiki-themes github-light github-light material-theme-palenight" language="html" style="">
        <span class="sqjlB">
          ComputedRef
        </span>
        
        <span class="sx-uw">
          <
        </span>
        
        <span class="sFfpx">
          boolean
        </span>
        
        <span class="sg-iE">
          |
        </span>
        
        <span class="sg-iE">
          null
        </span>
        
        <span class="sx-uw">
          >
        </span>
      </code>
    </td>
    
    <td>
      Whether the connection is authorized for stats (<code>
        null
      </code>
      
       while pending)
    </td>
  </tr>
</tbody>
</table>

## Example: Admin Dashboard

```vue
<script setup lang="ts">
const { total, versions, authorized } = useActiveConnections()
const currentBuildId = useRuntimeConfig().app.buildId

const stats = computed(() => {
  const current = versions.value[currentBuildId] || 0
  const outdated = total.value - current
  return {
    total: total.value,
    current,
    outdated,
    percentUpToDate: total.value ? Math.round((current / total.value) * 100) : 100
  }
})
</script>

<template>
  <div v-if="authorized" class="grid grid-cols-4 gap-4">
    <div class="stat">
      <span class="label">Total Users</span>
      <span class="value">{{ stats.total }}</span>
    </div>
    <div class="stat">
      <span class="label">On Latest</span>
      <span class="value text-green">{{ stats.current }}</span>
    </div>
    <div class="stat">
      <span class="label">On Old Version</span>
      <span class="value text-orange">{{ stats.outdated }}</span>
    </div>
    <div class="stat">
      <span class="label">Rollout Progress</span>
      <span class="value">{{ stats.percentUpToDate }}%</span>
    </div>
  </div>
  <div v-else>
    Not authorized for live stats
  </div>
</template>
```

## Custom Behavior with Hooks

For advanced use cases, you can use hooks directly instead of the composable.

### Client-Side: `skew:message`

Listen to all messages from the SSE/WebSocket connection:

```ts [plugins/custom-stats.client.ts]
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks.hook('skew:message', (message) => {
    if (message.type === 'stats') {
      // Custom handling
      console.log('Connections:', message.total)
      console.log('Versions:', message.versions)
    }
  })
})
```

### Server-Side: Connection Lifecycle

Build custom tracking logic with Nitro hooks:

```ts [server/plugins/custom-tracking.ts]
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('skew:connection:open', ({ id, version, route, ip, send }) => {
    console.log(`Client ${id} connected on ${version} from ${ip}`)

    // Send custom welcome message
    send({ type: 'welcome', serverTime: Date.now() })
  })

  nitroApp.hooks.hook('skew:connection:close', ({ id }) => {
    console.log(`Client ${id} disconnected`)
  })
})
```

See [Nuxt Hooks](/docs/skew-protection/api/nuxt-hooks) and [Nitro Hooks](/docs/skew-protection/nitro-api/nitro-hooks) for full documentation.
