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-tiles-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$$system:createdBy— who spawned this instance
$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-tiles-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-tiles-sdk/v1/shared.js works anywhere you have a ctx (mutators, query callbacks, subscribe callbacks):
typescript
import { getUserInfo } from "poe-tiles-sdk/v1/shared.js";
const info = await getUserInfo(ctx, ctx.userId);
// info?.username, info?.displayName, info?.profilePicture$$system:createdBy — who spawned this instance
Records the app instance that originally opened the current one (via apps.openChild or analogous platform entry point). Useful for "open the parent" affordances, attribution in shared timelines, and analytics.
typescript
type CreatedByValue = {
storeTypeId: string;
instanceId: string;
};
// In a mutator or subscribe/query callback
const spawner = await ctx.table("$$system").get("createdBy");
if (spawner) {
// spawner.storeTypeId, spawner.instanceId
}Semantics:
- First-writer-wins. Written by the platform on the first request that arrives with a
parentorigin. Never overwritten — even if a different parent later opens the same instance, the original spawner stays recorded. - Absent for root apps. Apps opened directly (not via
apps.openChild) have nocreatedByrow. - Absent for cross-store fan-out targets. Instances first reached only via a system mutator's
mutateExternal(e.g. a room dispatchingaddUserto a member) do NOT recordcreatedBy— the row reflects only direct opens. - Absent for legacy instances. Stores that existed before this feature shipped will not have the row.
- Not used for trust.
createdByis observational. Authorize decisions continue to use the live origin and$$system:room.
App code must always tolerate undefined.
Do Not Write to These Tables
App mutators can read but not write $userInfo, $users, $permissions, or $$system. Only the platform's built-in system mutators and the platform's beforeMutations hook produce changes. Treat them as read-only tables populated by the platform.