Skip to content

User Context

Read lightweight user context from backend extensions, including app visibility and account role. This is useful for extensions that want to avoid interrupting active users, defer background tasks until a user is away, or target operator/admin-only notices.

No permission is required. This is a free-tier utility.

Usage

const visible = await spindle.users.isVisible()
const role = await spindle.users.getRole()

if (visible) {
  spindle.log.info('User is actively viewing the app')
} else if (role === 'operator') {
  spindle.log.info('User is away — safe to send a push notification')
}

Methods

spindle.users.isVisible(userId?)

Returns true if the user has the app visible in at least one browser tab or PWA window. Returns false if all sessions are hidden/backgrounded, or the user has no active WebSocket connections.

Parameter Type Description
userId string? Target user (operator-scoped extensions only; user-scoped infers from owner)

Returns: Promise<boolean>

spindle.users.getRole(userId?)

Returns the target user's Lumiverse role as exposed to extensions.

Parameter Type Description
userId string? Target user (operator-scoped extensions only; user-scoped infers from owner)

Returns: Promise<'operator' | 'admin' | 'user'>

Lumiverse's internal owner account is reported as operator. Admin accounts are reported as admin, and standard accounts are reported as user.

How It Works

The Lumiverse frontend automatically reports page visibility to the backend over the WebSocket connection using the Page Visibility API. The backend tracks visibility per-user, per-session:

  • When a tab/window gains focus or becomes visible, the session is marked visible
  • When a tab/window is hidden or backgrounded, the session is marked hidden
  • When a WebSocket disconnects (tab closed, app quit), the session is removed

isVisible() returns true if any of the user's sessions are currently visible. This correctly handles multiple tabs, multiple devices, and PWA windows.

Example: Deferred Push Notification

async function sendNudgeIfAway(userId: string, message: string) {
  const visible = await spindle.users.isVisible(userId)

  if (visible) {
    // User is here — use an in-app toast instead
    spindle.toast.info(message)
  } else {
    // User is away — send a push notification
    await spindle.push.send({
      title: 'Hey!',
      body: message,
    }, userId)
  }
}

Example: Skip Background Work While Active

async function runMaintenanceTask() {
  const visible = await spindle.users.isVisible()

  if (visible) {
    spindle.log.info('User is active, deferring maintenance')
    setTimeout(runMaintenanceTask, 5 * 60 * 1000)
    return
  }

  // Safe to do expensive background work
  await performMaintenance()
}

Example: Operator-Only Update Toast

async function notifyIfOperator(userId: string, hasUpdate: boolean) {
  if (!hasUpdate) return

  const role = await spindle.users.getRole(userId)
  if (role !== 'operator') return

  spindle.toast.info('A new extension update is available.', {
    title: 'Update available',
    userId,
  })
}

Combining with Push Notifications

Push notifications are automatically suppressed by the service worker when the app is focused. However, isVisible() lets you make smarter decisions before generating the notification content — avoiding unnecessary LLM calls, database queries, or other expensive work when the user is right there in the app.

Note

For user-scoped extensions, the user context is inferred automatically. For operator-scoped extensions, pass userId explicitly.