---
title: Presence Tracking
description: Learn how to track user presence in real-time with NoLag. Know who is online, handle join/leave events, and show user status.
---

# Presence Tracking

Know who's online in real-time. Track user presence across your project and handle join/leave events.

## What is Presence?

Presence tracking allows you to see which actors (users, devices, or servers) are currently connected to your project. This is essential for features like:

- Online/offline indicators in chat apps
- Showing who's viewing a document
- Live user counts
- Typing indicators

**Client-level:** Presence events (`presence:join`, `presence:leave`, `presence:update`) are tracked at the client level, not room-scoped. Use `client.on()` to listen for these events. For observing presence across rooms, use [Lobbies](/docs/concepts/lobbies).

## Setting Presence

Set your presence after connecting. Presence data can include any custom fields you need:

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'

const client = NoLag('your_access_token')
await client.connect()

// Set presence at the client level
client.setPresence({
  username: 'Alice',
  status: 'online',
  avatar: 'https://example.com/alice.jpg'
})

// Presence events are client-level, not room-scoped
client.on('presence:join', (actor) => {
  console.log(`${actor.presence.username} joined`)
})

client.on('presence:leave', (actor) => {
  console.log(`${actor.presence.username} left`)
})

client.on('presence:update', (actor) => {
  console.log(`${actor.presence.username} updated status to ${actor.presence.status}`)
})
```
```python [Python]
from nolag import NoLag

client = NoLag('your_access_token')
await client.connect()

# Set presence at the client level
await client.set_presence({
    'username': 'Alice',
    'status': 'online',
    'avatar': 'https://example.com/alice.jpg'
})

# Presence events are client-level, not room-scoped
def on_join(actor):
    print(f"{actor.presence['username']} joined")

def on_leave(actor):
    print(f"{actor.presence['username']} left")

def on_update(actor):
    print(f"{actor.presence['username']} updated status")

client.on('presence:join', on_join)
client.on('presence:leave', on_leave)
client.on('presence:update', on_update)
```
```go [Go]
package main

import (
    "fmt"

    nolag "github.com/NoLagApp/nolag-go"
)

func main() {
    client := nolag.New("your_access_token")
    client.Connect()

    // Set presence at the client level
    client.SetPresence(map[string]any{
        "username": "Alice",
        "status":   "online",
        "avatar":   "https://example.com/alice.jpg",
    })

    // Listen for presence events (join, leave, update)
    client.On("presence", func(args ...any) {
        topic, _ := args[0].(string)
        data, _ := args[1].(map[string]any)
        fmt.Printf("Presence event on %s: %v\n", topic, data)
    })
}
```

## Presence Events

Listen for these events to track when actors come online, go offline, or update their status:

- `presence:join` - An actor connected and set their presence
- `presence:leave` - An actor disconnected
- `presence:update` - An actor updated their presence data

## Getting Current Presence

Get a list of all actors currently present. Note that `getPresence()` returns all actors from the local cache (not room-scoped):

```typescript [TypeScript]
// Get all present actors from local cache (client-level)
const actors = client.getPresence()
console.log('Users online:', actors.length)

actors.forEach((actor) => {
  console.log(`- ${actor.presence.username} (${actor.actorTokenId})`)
})

// Fetch fresh presence list from server
const freshList = await client.fetchPresence()
console.log('Server says online:', freshList.length)
```
```python [Python]
# Get all present actors from local cache (client-level)
actors = client.get_all_presence()
print('Users online:', len(actors))

for actor in actors:
    print(f"- {actor.presence['username']} ({actor.actor_token_id})")

# Get a specific actor's presence
alice = client.get_presence('actor_token_id_here')
if alice:
    print(f"Alice is {alice.presence['status']}")
```
```go [Go]
// Get presence for actors on a topic
room := client.SetApp("my-app").SetRoom("general")
actors, err := client.GetPresence(room.Prefix() + "/messages")
if err == nil {
    fmt.Println("Users online:", len(actors))

    for _, actor := range actors {
        fmt.Printf("- %s (%s)\n", actor.Presence["username"], actor.ActorTokenID)
    }
}
```

### Local Cache vs Server Fetch

- **JavaScript:** `client.getPresence()` returns all, `client.getPresence(actorId)` returns one, `client.fetchPresence()` fetches from server
- **Python:** `client.get_all_presence()` returns all, `client.get_presence(actor_token_id)` returns one
- **Go:** `client.GetPresence(topic)` fetches presence for a specific topic from the server

The local cache is automatically updated when you receive presence events.

## Updating Presence

Update your presence data at any time by calling `setPresence()` again:

```typescript [TypeScript]
// Update your presence data (e.g., status change)
client.setPresence({
  username: 'Alice',
  status: 'away',
  lastActive: Date.now()
})

// Client-level presence is automatically re-sent on reconnect
```
```python [Python]
# Update your presence data (e.g., status change)
await client.set_presence({
    'username': 'Alice',
    'status': 'away',
    'lastActive': time.time()
})

# Client-level presence is automatically re-sent on reconnect
```
```go [Go]
// Update your presence data (e.g., status change)
client.SetPresence(map[string]any{
    "username":   "Alice",
    "status":     "away",
    "lastActive": time.Now().Unix(),
})

// Client-level presence is automatically re-sent on reconnect
```

## Typing Indicators

A common use case is showing typing indicators. Use presence updates with a debounce:

```typescript [TypeScript]
// For typing indicators, update presence with typing status
let typingTimeout: NodeJS.Timeout

function sendTyping() {
  client.setPresence({
    username: 'Alice',
    status: 'online',
    isTyping: true
  })

  // Clear typing after 3 seconds of inactivity
  clearTimeout(typingTimeout)
  typingTimeout = setTimeout(() => {
    client.setPresence({
      username: 'Alice',
      status: 'online',
      isTyping: false
    })
  }, 3000)
}

// Listen for typing from others (client-level event)
client.on('presence:update', (actor) => {
  if (actor.presence.isTyping) {
    showTypingIndicator(actor.presence.username)
  } else {
    hideTypingIndicator(actor.presence.username)
  }
})
```
```python [Python]
# For typing indicators, update presence with typing status
typing_task = None

async def send_typing():
    global typing_task

    await client.set_presence({
        'username': 'Alice',
        'status': 'online',
        'isTyping': True
    })

    # Clear typing after 3 seconds of inactivity
    if typing_task:
        typing_task.cancel()

    async def clear_typing():
        await asyncio.sleep(3)
        await client.set_presence({
            'username': 'Alice',
            'status': 'online',
            'isTyping': False
        })

    typing_task = asyncio.create_task(clear_typing())

# Listen for typing from others (client-level event)
def on_presence_update(actor):
    if actor.presence.get('isTyping'):
        show_typing_indicator(actor.presence['username'])
    else:
        hide_typing_indicator(actor.presence['username'])

client.on('presence:update', on_presence_update)
```
```go [Go]
// For typing indicators, update presence with typing status
var typingTimer *time.Timer

func sendTyping() {
    client.SetPresence(map[string]any{
        "username": "Alice",
        "status":   "online",
        "isTyping": true,
    })

    // Clear typing after 3 seconds of inactivity
    if typingTimer != nil {
        typingTimer.Stop()
    }
    typingTimer = time.AfterFunc(3*time.Second, func() {
        client.SetPresence(map[string]any{
            "username": "Alice",
            "status":   "online",
            "isTyping": false,
        })
    })
}

// Listen for presence events
client.On("presence", func(args ...any) {
    data, _ := args[1].(map[string]any)
    if isTyping, ok := data["isTyping"].(bool); ok && isTyping {
        showTypingIndicator(data["username"].(string))
    } else {
        hideTypingIndicator(data["username"].(string))
    }
})
```

## Actor Presence Structure

Each actor presence object contains:

```typescript [TypeScript]
interface ActorPresence {
  actorTokenId: string    // Unique actor identifier
  actorType: 'device' | 'user' | 'server'
  presence: {             // Your custom presence data
    username?: string
    status?: string
    // ... any other fields you set
  }
  joinedAt?: number       // Timestamp when actor connected
}
```

## Best Practices

- **Keep presence data small** - Only include necessary information (username, status, avatar URL)
- **Use debouncing** - For typing indicators, debounce updates to avoid flooding
- **Handle reconnections** - Client-level presence is automatically re-sent on reconnect
- **Consider privacy** - Let users opt out of presence tracking if needed
- **Use fetchPresence() sparingly** - The local cache is usually sufficient

## Next Steps

- [Observe presence across rooms with Lobbies](/docs/concepts/lobbies)
- [Learn about Topics](/docs/concepts/topics)
- [Organize with Rooms](/docs/concepts/rooms)
- [Build a Chat App](/docs/guides/chat-app)
