Skip to Content

Presence + Metadata

This guide explains how to combine presence tracking with metadata to build rich user awareness features in your Mesh application.

The Challenge

Presence in Mesh only tracks connection IDs, not user information. This is by design, as it keeps the presence system lightweight and flexible. However, most applications need to show more than just connection IDs — they need usernames, avatars, status messages, and other user information.

The solution is to combine presence tracking with connection metadata.

Setting Up Connection Metadata

First, set up connection metadata on the server to store user information:

// Set user metadata when they connect server.onConnection(async (connection) => { const userId = getUserIdSomehow(connection); if (userId) { const user = await db.users.findById(userId); await server.connectionManager.setMetadata(connection, { userId: user.id, username: user.username, avatar: user.avatarUrl, role: user.role, }); } });

Combining with Presence on the Client

Now, on the client side, you can combine presence updates with metadata:

const { success, present } = await client.subscribePresence( "lobby", async (update) => { const metadata = await client.getConnectionMetadata(update.connectionId); if (update.type === "join") { console.log(`${metadata.username} joined the lobby`); addUserToUI(metadata); } else if (update.type === "leave") { console.log(`${metadata.username} left the lobby`); removeUserFromUI(metadata.userId); } else if (update.type === "state") { if (update.state) { console.log(`${metadata.username} state:`, update.state); updateUserStateInUI(metadata.userId, update.state); } else { console.log(`${metadata.username}'s state was cleared`); clearUserStateInUI(metadata.userId); } } } ); if (success) { const allMetadata = await Promise.all( present.map((connectionId) => client.getConnectionMetadata(connectionId)) ); allMetadata.forEach(addUserToUI); }

Optimizing Metadata Lookups

Use a client-side cache to avoid redundant fetches:

const metadataCache = new Map(); async function getUserMetadata(connectionId) { if (metadataCache.has(connectionId)) { return metadataCache.get(connectionId); } const metadata = await client.getConnectionMetadata(connectionId); metadataCache.set(connectionId, metadata); return metadata; } client.onDisconnect(() => { metadataCache.clear(); });

Handling Multiple Connections per User

Mesh tracks presence per connection, not per user. If a user opens multiple tabs or devices, each will appear separately.

To deduplicate presence by user ID, you can use the built-in createDedupedPresenceHandler utility.

import { createDedupedPresenceHandler } from "@mesh-kit/core/client-utils"; const handler = createDedupedPresenceHandler({ // Group connections by userId from metadata getGroupId: async (connectionId) => { const metadata = await client.getConnectionMetadata(connectionId); return metadata.userId ?? connectionId; }, // Called whenever the deduplicated group state changes onUpdate: (groups) => { for (const [userId, group] of groups) { updateUserPresenceUI(userId, { tabCount: group.members.size, state: group.state, representative: group.representative, lastUpdated: group.timestamp, }); } }, }); await client.subscribePresence("lobby", handler);

Each group object contains:

  • members - a Set of all active connection IDs for that user
  • state - the latest presence state (e.g. { typing: true })
  • timestamp - when that state was last updated
  • representative - the connection ID that sent the latest state

You can use this to:

  • Show only one user per row in your UI
  • Track how many tabs/devices a user has open
  • Always show the freshest state across all their connections

Presence States with Metadata

// Publish state await client.publishPresenceState("document:123", { state: { action: "editing", section: "introduction", cursorPosition: { line: 5, column: 10 }, }, expireAfter: 10000, }); // Handle updates await client.subscribePresence("document:123", async (update) => { if (update.type === "state") { const metadata = await getUserMetadata(update.connectionId); if (update.state) { const { action, section, cursorPosition } = update.state; if (action === "editing") { console.log(`${metadata.username} is editing the ${section} section`); showUserCursor(metadata.userId, cursorPosition); } } else { console.log(`${metadata.username} stopped editing`); hideUserCursor(metadata.userId); } } });

Best Practices

  1. Keep metadata focused
    Store only what you need for presence-related features.

  2. Cache metadata client-side
    Avoid repeated lookups with a local cache.

  3. Consider user-level presence
    Deduplicate by userId if needed for your UI.

  4. Use batch lookups when resolving many connections
    Promise.all(...) is great for initial joins.

  5. Handle metadata errors gracefully
    Always check for nulls or missing fields and fallback accordingly.

Last updated on
© 2025