Skip to main content
The User Presence Status API allows you to manage user availability status (online, away, busy, DND, offline), custom status messages with emojis, and activity tracking. All methods require authentication.

Authentication

All User Presence Status API methods require authentication via the AuthMiddleware. Include a valid bearer token in your requests.

Status Types

The API supports the following presence statuses:
  • online - User is active and available
  • away - User is inactive or temporarily unavailable
  • busy - User is active but should not be disturbed
  • dnd - Do Not Disturb mode (suppresses notifications)
  • offline - User is not available

Methods

Update Presence Status

Update the user’s presence status and optional custom message. The userId is automatically set from the authenticated user.
const client = yield* RpcClient

const result = yield* client["userPresenceStatus.update"]({
  status: "busy",
  customMessage: "In a meeting",
  statusEmoji: "📅",
  statusExpiresAt: new Date(Date.now() + 3600000), // Expires in 1 hour
  suppressNotifications: true
})
status
'online' | 'away' | 'busy' | 'dnd' | 'offline'
User’s presence status. If not provided, defaults to existing status or “online”
customMessage
string | null
Custom status message to display (e.g., “In a meeting”, “On vacation”). Set to null to clear.
statusEmoji
string | null
Emoji to display with the status (e.g., ”📅”, ”🌴”). Set to null to clear.
statusExpiresAt
Date | null
When the custom status should automatically clear. Set to null for no expiration.
activeChannelId
ChannelId | null
ID of the channel the user is currently viewing/active in. Set to null to clear.
suppressNotifications
boolean
Whether to suppress notifications while this status is active
data
UserPresenceStatus
required
The updated presence status object
transactionId
TransactionId
required
Transaction ID for optimistic updates
Errors:
  • UnauthorizedError - User is not authenticated
  • InternalServerError - Unexpected server error
Implementation Notes:
  • All parameters are optional and merge with existing values
  • If no status record exists, one is created with default values
  • Updates the lastSeenAt timestamp as a heartbeat
  • Performed within a database transaction

Heartbeat

Lightweight heartbeat to update the lastSeenAt timestamp. Used for reliable offline detection - if no heartbeat is received within the timeout period, the user is marked offline by a server-side cron job.
const client = yield* RpcClient

// Send heartbeat every 30 seconds
const result = yield* client["userPresenceStatus.heartbeat"]({})
console.log("Last seen:", result.lastSeenAt)
payload
{}
Empty object (no parameters)
lastSeenAt
Date
required
Updated timestamp of when the user was last seen active
Errors:
  • UnauthorizedError - User is not authenticated
  • InternalServerError - Unexpected server error
Implementation Notes:
  • If no presence status record exists, creates one with “online” status
  • Only updates the lastSeenAt field, no other status changes
  • Very lightweight operation designed for frequent polling
  • Typical usage: Send heartbeat every 30-60 seconds from active clients

Clear Custom Status

Clears the user’s custom status (emoji, message, and expiration). Does not affect the user’s presence status (online/away/etc).
const client = yield* RpcClient

const result = yield* client["userPresenceStatus.clearStatus"]({})
payload
{}
Empty object (no parameters)
data
UserPresenceStatus
required
The updated presence status object with cleared custom status fields
transactionId
TransactionId
required
Transaction ID for optimistic updates
Errors:
  • UnauthorizedError - User is not authenticated
  • InternalServerError - Unexpected server error
Implementation Notes:
  • Clears customMessage, statusEmoji, and statusExpiresAt
  • Preserves the status field (online/away/busy/dnd/offline)
  • Preserves the activeChannelId field
  • Resets suppressNotifications to false
  • Updates the lastSeenAt timestamp

Usage Patterns

import { Effect } from "effect"
import { RpcClient } from "@hazel/domain/rpc"

const setTemporaryStatus = Effect.gen(function* () {
  const client = yield* RpcClient
  
  // Set status that expires in 2 hours
  const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000)
  
  const result = yield* client["userPresenceStatus.update"]({
    customMessage: "On lunch break",
    statusEmoji: "🍕",
    statusExpiresAt: expiresAt,
    status: "away"
  })
  
  return result.data
})

Offline Detection

Hazel Chat uses a heartbeat-based system for reliable offline detection:
  1. Client sends heartbeats: Active clients should call heartbeat() every 30-60 seconds
  2. Server tracks lastSeenAt: Each heartbeat updates the lastSeenAt timestamp
  3. Server-side cron job: A background job periodically checks for stale lastSeenAt timestamps
  4. Automatic offline marking: Users with no recent heartbeat are automatically marked as offline
This approach ensures accurate presence status even if clients disconnect unexpectedly.

Error Handling

import { Effect } from "effect"
import { RpcClient } from "@hazel/domain/rpc"

const updateStatusSafe = Effect.gen(function* () {
  const client = yield* RpcClient
  
  const result = yield* client["userPresenceStatus.update"]({
    status: "busy",
    customMessage: "In a meeting"
  }).pipe(
    Effect.catchTag("UnauthorizedError", () =>
      Effect.fail(new Error("Not authenticated"))
    ),
    Effect.catchTag("InternalServerError", (error) =>
      Effect.fail(new Error(`Server error: ${error.message}`))
    )
  )
  
  return result
})

Build docs developers (and LLMs) love