---
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-08-19T01:24:58.502Z"
---

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.

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

## 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)

::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.
::

## 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()`{lang="ts"} 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(() => {
      // WebSocket events lack the H3 context required by getUserSession.
      return 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(() => {
            // Invalid or expired cookies represent an unauthenticated connection.
            return null
          })
          // Note: unsealSession returns { data: { user } }, not { user } directly
          const email = unsealed?.data?.user?.email
          if (email && ADMIN_EMAILS.includes(email)) {
            authorize()
          }
        }
      }
    }
  })
})
```

::note
The key difference: `getUserSession()`{lang="ts"} returns `{ user: { email } }` while `unsealSession()`{lang="ts"} returns `{ data: { user: { email } } }`.
::

## Using the Composable

The [`useActiveConnections()`{lang="ts"}](/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:**

| Property      | Type                                               | Description                                                                                   |
| ------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `total`       | `ComputedRef<number>`{lang="html"}                 | Total active connections                                                                      |
| `versions`    | `ComputedRef<Record<string, number>>`{lang="html"} | Map of buildId → connection count                                                             |
| `routes`      | `ComputedRef<Record<string, number>>`{lang="html"} | Map of route → connection count (requires `routeTracking`)                                    |
| `connections` | `ComputedRef<ConnectionInfo[]>`{lang="html"}       | Individual connections with `id`, `version`, `route`, and `ip` (requires `ipTracking` for IP) |
| `yourId`      | `ComputedRef<string \| undefined>`{lang="html"}    | Your connection ID to identify yourself in the list                                           |
| `authorized`  | `ComputedRef<boolean \| null>`{lang="html"}        | Whether the connection is authorized for stats (`null` while pending)                         |

## 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.