ReadonlyuserSubject this profile belongs to (ctx.sender).
ReadonlynamespaceNamespace this handle reads and writes ('default' for ctx.user).
Value at path, or undefined when missing or expired. T is an
unchecked convenience cast — validate untrusted shapes yourself.
Every live top-level key of this namespace as a plain object ({} when empty).
Write value at path (see UserSetOptions.ttl for expiry). undefined deletes.
Optionaloptions: UserSetOptionsApply several writes: each own enumerable property of values is a PATH
({ name: 'Juan', 'address.city': 'Bogotá' }), applied in object key
order like set; keys not mentioned are left alone. Paths, values,
ttl and array indexes (against the stored values) are validated before
anything is written, so an invalid batch writes nothing. All paths under
one top-level key become a single store write; the batch as a whole is not
atomic: each top-level key is its own write, so if the STORE fails one of
them the others may already be persisted. The call rejects with the first
store error only after every write of the batch has settled, so the next
write (or erasure) of this user never overlaps it.
Optionaloptions: UserSetOptionsRemove path (a top-level key or a nested member). Missing paths are a no-op.
Remove every key of THIS namespace for this user (other namespaces are untouched).
Same API scoped to namespace name. Throws UserDataError('INVALID_NAMESPACE') synchronously.
Persistent, per-user key/value profile —
ctx.userin handlers andbot.user(userId)anywhere else.Scope. Keyed by the canonical sender (
ctx.sender, the provider-resolvedsenderId), so it is shared across every chat that person writes from (DM and groups) and survives conversations and restarts (with a persistent store). It lives inside the bot's store namespace (tenant / environment / bot / provider / connection), so two bots sharing one backend never see each other's profiles.Namespaces.
ctx.useris the'default'namespace;ctx.user.ns('game')returns the same API scoped to'game'. Namespace names are flat and absolute (ctx.user.ns('a').ns('b')is namespace'b'). Use one per feature/plugin to avoid key collisions.Paths. Every method takes a dot path:
'name','address.city','items.0'. The FIRST segment is the top-level key, which is the unit the store persists atomically; deeper segments are resolved inside its JSON value (array elements by index). Keys cannot contain dots;__proto__,prototype,constructor, empty segments and control characters are rejected withUserDataError('INVALID_PATH').Values are JSON data: they are deep-copied through JSON on write (a
Datebecomes its ISO string,undefinedmembers vanish) and every read returns a fresh copy. BigInt, functions, symbols and cycles reject withUserDataError('INVALID_VALUE'). Writingundefineddeletes the path.Concurrency. The dispatcher's per-chat FIFO does NOT serialise one user across several chats (the same person can be handled in a group and in a DM at the same time), so the profile serialises writes itself: inside one process, every write of a user (
set/update/delete/clearand the bot's subject erasure) runs in call order, one at a time. A top-levelset/deleteis a single atomic store write; a nested write is a read-modify-write of its top-level key. Across several bot replicas sharing a store, top-level writes stay atomic (last writer wins) but two replicas writing nested paths of the SAME top-level key concurrently can lose one update — keep such keys owned by one replica or write whole top-level values. Reads never wait for writes;awaityour writes before reading them back.Example