Appearance
Getting User Info of Members
Poe auto-populates three $-prefixed system tables when users connect to an app instance. Use them to render member lists, leaderboards, chat avatars, or anywhere else you need a user's display name or profile picture. Apps using poe-apps-sdk's defineSchema get these tables typed automatically — you do not declare them.
Table of Contents
$userInfo— profile data$users— membership roster$permissions— permission grants- Current user in the UI
getCurrentUserIdhelpergetUserInfohelper
$userInfo — profile data
ItemKey is the userId.
typescript
type PoeUserInfo = {
userId: string;
username: string;
displayName: string;
profilePicture: string; // URL
isDev?: boolean; // synthetic dev user
};Retained after a user is removed, so apps can still render historical content (past messages, past moves) with the original author's name and avatar.
$users — membership roster
Tracks who has joined the store instance. ItemKey is the userId.
typescript
type UserMembership = {
userId: string;
addedAt: number; // timestamp
addedBy: string; // userId of who added them, "system" for auto-add
removedAt?: number; // set when removed
removedBy?: string;
isDev?: boolean;
};
// All current members
store.subscribe(
(tx) => tx.table("$users").entries().toArray(),
(entries) => {
const members = entries
.map(([, v]) => v as UserMembership)
.filter((u) => !u.removedAt);
},
);$permissions — permission grants
ItemKey is {userId}/{permission}. Rows are hard-deleted on revocation.
typescript
type UserPermission = {
userId: string;
permission: string;
grantedAt: number;
grantedBy: string;
};
// In a mutator
const perm = await ctx.table("$permissions").get(`${ctx.userId}/admin`);
const isAdmin = perm !== undefined;Current user in the UI
ctx.userId is only exposed to mutators and subscribe/query callbacks — not to code that just holds a store reference. Inside a subscribe/query callback, read the current user's row directly:
typescript
const [myInfo, setMyInfo] = useState<PoeUserInfo | null>(null);
useEffect(() => {
const unsub = store.subscribe(
(tx) => tx.table("$userInfo").get(tx.userId),
(info) => setMyInfo(info ?? null),
);
return unsub;
}, [store]);getCurrentUserId helper
When you only need the userId itself (not the full profile) and you're outside a subscribe/query callback — e.g. in an async init hook, a SolidJS resource, or a one-shot read at app mount — use getCurrentUserId:
typescript
import { getCurrentUserId } from "poe-apps-sdk/v1/client.js";
// Once at app mount:
const userId = await getCurrentUserId(store);It runs a one-shot query that resolves to tx.userId. For per-render reactive access, prefer reading tx.userId inside a store.subscribe() query callback (see "Current user in the UI" above).
getUserInfo helper
getUserInfo from poe-apps-sdk/v1/shared.js works anywhere you have a ctx (mutators, query callbacks, subscribe callbacks):
typescript
import { getUserInfo } from "poe-apps-sdk/v1/shared.js";
const info = await getUserInfo(ctx, ctx.userId);
// info?.username, info?.displayName, info?.profilePictureDo Not Write to These Tables
App mutators can read but not write $userInfo, $users, $permissions. Only the platform's built-in system mutators (addUser, removeUser, permission grants/revokes) produce changes. Treat them as read-only tables populated by the platform.