Skip to main content
A Session represents a single WebSocket connection. Each connected client has its own session with a unique ID and associated value from authentication.

Session Lifecycle

Client connects (HTTP upgrade request)


┌─────────────┐
│   OnAuth    │ ──── Error ────▶ Connection rejected (401)
└─────────────┘

   Returns value


┌─────────────┐
│  Session    │ ◀──── Session created with ID and value
│  Created    │
└─────────────┘


┌─────────────┐
│   Active    │ ◀──── Send/Receive messages
└─────────────┘       OnMessage called for each message

   Client disconnects
   or server closes
   or queue full


┌─────────────┐
│  OnClose    │ ──── Session destroyed
└─────────────┘

Session Methods

ID

Returns the session’s unique identifier.
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
    sessionID := s.ID()
    log.Printf("Message from session: %s", sessionID)
}

Value

Returns the value stored during authentication.
OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
    // Store user info
    return UserInfo{ID: "123", Name: "Alice"}, nil
}

OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
    // Retrieve user info
    user := s.Value().(UserInfo)
    log.Printf("Message from user: %s", user.Name)
}

Send

Queues a message for delivery to the client. Non-blocking.
// Send a message
err := s.Send(live.Message{
    Topic: "notification",
    Data:  []byte(`{"title":"Hello"}`),
})

if err == live.ErrSessionClosed {
    log.Println("Session already closed")
} else if err == live.ErrQueueFull {
    log.Println("Queue full, session closed")
}

Close

Closes the connection gracefully.
// Kick a user
s.Close()

IsClosed

Checks if the session is closed.
if s.IsClosed() {
    return // Don't send to closed session
}

CloseError

Returns the error that caused the session to close.
OnClose: func(s *live.Session, err error) {
    closeErr := s.CloseError()
    if closeErr == live.ErrQueueFull {
        log.Println("Client couldn't keep up")
    }
}

Subscription Management

Subscriptions are managed through the server, not the session:
// Subscribe a session to a topic
server.Subscribe(s, "chat:room1")

// Unsubscribe from a topic
server.Unsubscribe(s, "chat:room1")
When a session closes, it’s automatically unsubscribed from all topics.

Common Patterns

User Authentication

type UserInfo struct {
    ID   string
    Name string
    Role string
}

server := live.New(live.Options{
    OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
        token := r.Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            return nil, err
        }
        return user, nil
    },

    OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
        user := s.Value().(UserInfo)

        // Check permissions
        if topic == "admin" && user.Role != "admin" {
            s.Send(live.Message{Topic: "error", Data: []byte(`"unauthorized"`)})
            return
        }
        // Handle message...
    },
})

Auto-Subscribe on Connect

Since there’s no OnConnect hook, use OnMessage with a special “init” topic:
OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
    user := s.Value().(UserInfo)

    switch topic {
    case "init":
        // Subscribe to default topics
        server.Subscribe(s, "user:"+user.ID)
        server.Subscribe(s, "global")

        // Send welcome message
        s.Send(live.Message{
            Topic: "welcome",
            Data:  []byte(`{"status":"connected"}`),
        })

    case "subscribe":
        // Handle explicit subscription requests
        var req struct{ Topic string `json:"topic"` }
        json.Unmarshal(data, &req)
        server.Subscribe(s, req.Topic)
    }
}
Client sends init on connect:
ws.onopen = () => {
    ws.send(JSON.stringify({topic: 'init', data: {}}));
};

Presence Tracking

var onlineUsers sync.Map

server := live.New(live.Options{
    OnAuth: func(ctx context.Context, r *http.Request) (any, error) {
        userID := r.URL.Query().Get("user")
        return userID, nil
    },

    OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
        userID := s.Value().(string)

        if topic == "init" {
            // Mark user online
            onlineUsers.Store(userID, s.ID())

            // Subscribe to presence
            server.Subscribe(s, "presence")

            // Notify others
            server.Publish("presence", []byte(`{"type":"join","user":"`+userID+`"}`))
        }
    },

    OnClose: func(s *live.Session, err error) {
        userID := s.Value().(string)

        // Mark offline
        onlineUsers.Delete(userID)

        // Notify others
        server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`))
    },
})

Direct Messaging

// Store sessions by user ID
var userSessions sync.Map  // userID -> *live.Session

OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
    userID := s.Value().(string)

    switch topic {
    case "init":
        userSessions.Store(userID, s)

    case "dm":
        var req struct {
            To   string `json:"to"`
            Text string `json:"text"`
        }
        json.Unmarshal(data, &req)

        // Find recipient
        if recipient, ok := userSessions.Load(req.To); ok {
            sess := recipient.(*live.Session)
            msg, _ := json.Marshal(map[string]string{
                "from": userID,
                "text": req.Text,
            })
            sess.Send(live.Message{Topic: "dm", Data: msg})
        }
    }
},

OnClose: func(s *live.Session, err error) {
    userID := s.Value().(string)
    userSessions.Delete(userID)
},

Room Management

OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) {
    userID := s.Value().(string)

    switch topic {
    case "join_room":
        var req struct{ Room string `json:"room"` }
        json.Unmarshal(data, &req)

        // Subscribe to room
        server.Subscribe(s, "room:"+req.Room)

        // Notify room
        msg, _ := json.Marshal(map[string]string{"type": "join", "user": userID})
        server.Publish("room:"+req.Room, msg)

    case "leave_room":
        var req struct{ Room string `json:"room"` }
        json.Unmarshal(data, &req)

        // Notify room
        msg, _ := json.Marshal(map[string]string{"type": "leave", "user": userID})
        server.Publish("room:"+req.Room, msg)

        // Unsubscribe
        server.Unsubscribe(s, "room:"+req.Room)

    case "room_message":
        var req struct {
            Room string `json:"room"`
            Text string `json:"text"`
        }
        json.Unmarshal(data, &req)

        msg, _ := json.Marshal(map[string]string{
            "type": "message",
            "user": userID,
            "text": req.Text,
        })
        server.Publish("room:"+req.Room, msg)
    }
}

Errors

ErrorDescription
ErrSessionClosedSession is already closed
ErrQueueFullSend queue is full, session was closed

Best Practices

1. Store Minimal Data in Value

// Good: just the user ID
return userID, nil

// Okay: small struct
return UserInfo{ID: "123", Role: "admin"}, nil

// Avoid: large objects
return entireUserObject, nil  // Bad

2. Handle Close Gracefully

OnClose: func(s *live.Session, err error) {
    userID := s.Value().(string)

    // Clean up all resources
    onlineUsers.Delete(userID)
    userSessions.Delete(userID)

    // Notify others if needed
    server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`))
}

3. Check Before Sending

if !s.IsClosed() {
    s.Send(message)
}

4. Handle Reconnection

Clients will reconnect. Design your protocol to handle it:
// Client-side reconnection
function connect() {
    ws = new WebSocket(url);
    ws.onopen = () => {
        // Re-initialize on every connect
        ws.send(JSON.stringify({topic: 'init', data: {}}));
    };
    ws.onclose = () => setTimeout(connect, 1000);
}