Users & Profiles
The public identity surface. A profile is the whole of what one user can see about another — email is never exposed through this API. Profiles are platform-wide (cross-app): one profile per user, resolved the same way from every app.
type Profile = {
id: string | null // null for an account-deleted user
displayName: string | null
avatarUrl: string | null
handle: string | null
}
SDK#
await tallpond.auth.getUser() // → { id, profile } | null (null when signed out)
await tallpond.users(id).profile() // → Profile
await tallpond.users([id1, id2]).profiles() // → Record<id, Profile>
await tallpond.users.byHandle('alice') // → Profile (with id) — 404 if unclaimed
Profiles are read-only for apps. updateProfile exists on the SDK but the
gateway accepts it only from the tallpond dashboard (403 dashboard_only
otherwise): the profile is central platform identity rendered by every app, so
untrusted apps may never write it — the same rule as wallets and spend caps.
Users are never asked to choose a handle: one is generated for them at signup
and changed later, in the dashboard, if ever. They manage both fields there.
A user with no profile yet resolves to their id with null fields. An
account-deleted user resolves to the reserved deleted-user profile
({ id: null, displayName: "Deleted user" }).
Handles#
Handles are the platform-central username: optional, unique across all of
tallpond, 3–30 characters of a–z, 0–9, _. Because they're platform-wide,
@alice is the same person in every app — apps should not hand-roll their
own handle systems (app-local nicknames layered on top are fine; that's what
non-unique displayName and your own tables are for).
- Resolve:
GET /v1/users/by-handle/:handle(SDKusers.byHandle) returns the owner's profile including theirid— the building block for @-mentions, user search, and invite-by-handle (resource(id).membersinvites take a user id). Lookup is case-insensitive;404 not_foundwhen unclaimed. - Claim/change: platform-only, and always in the dashboard. Signup does
not ask: a handle is generated at consent, seeded from the display name
the user offered (
Ada Lovelace→ada_lovelace, with a random suffix when that is taken) and from nothing else when they offered none. Never seeded from an email address — a handle is public and an address is not. Choosing a globally unique name has four rejection paths, and asking for it before the user had seen the app they came for fired all four at the worst moment. Changing it later returns409 conflict/handle_takenwhen taken, and400 invalid_request/handle_reservedfor platform-reserved names (admin,support,tallpond, …). Apps cannot set, change, or clear handles. - Recycling cooldown: when a handle is given up (renamed away, or the
account is deleted) it is locked for 30 days — only its previous owner can
re-claim it during the window (
409/handle_cooldownfor anyone else). This keeps a rename from handing@aliceto an impersonator.
The author(...) projection#
On mounted (creator-owned) tables, select accepts the reserved author(...)
relation, which resolves the row's creator to their profile:
await tallpond.resource(roomId)
.table("messages")
.select("body, author(displayName, avatarUrl)")
Each row gains an author object with the requested fields (any of id,
displayName, avatarUrl, handle). The gateway resolves all rows' authors in
one batched query. Rows whose creator has deleted their account resolve to the
deleted-user profile — so every app renders deletion the same way, with no
per-app null-checking. author(...) is rejected on private and resource-owned
tables (they have no row creator).
Account deletion#
POST /v1/account/delete (dashboard only) deletes the caller's account across
every app at once. For each resource the user contributed to, the mount's
onOwnerDelete policy decides what the context sees afterward:
remove(default) — the links vanish; resource queries show no trace.tombstone— the rows stay in place, but the deleted profile makesauthor(...)render "Deleted user". Thread structure survives; authorship is anonymized.retain— the rows are reassigned to the resource, which keeps them at its own cost with authorship anonymized.
The user's private rows and all their files are then swept, resources they own are deleted (ownership transfer is a future option), and their profile is marked deleted. The append-only wallet ledger is retained for audit; session revocation is handled by the identity provider.