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
- aSet
of all active connection IDs for that userstate
- the latest presence state (e.g.{ typing: true }
)timestamp
- when that state was last updatedrepresentative
- 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
-
Keep metadata focused
Store only what you need for presence-related features. -
Cache metadata client-side
Avoid repeated lookups with a local cache. -
Consider user-level presence
Deduplicate byuserId
if needed for your UI. -
Use batch lookups when resolving many connections
Promise.all(...)
is great for initial joins. -
Handle metadata errors gracefully
Always check for nulls or missing fields and fallback accordingly.