Users

The Users resource is the caller's own account plus every read-only lookup against other users. The literal /@me segment is always the calling user; :userId is anyone else.

@me routes are mounted before parameterized ones so the router matches the literal first. Self-actions never need a permission check — acting on yourself is always allowed. Acting on :userId always gates on users.read or users.write.


The User object

The full user row, as returned by GET /v1/users/@me. Public-facing lookups return a narrower PublicUser shape.

  • Name
    id
    Type
    string
    Description

    Snowflake — 64-bit decimal string, never an integer.

  • Name
    email
    Type
    string
    Description

    Lowercase. Unique. Source of identity.

  • Name
    username
    Type
    string | null
    Description

    2–32 chars, [a-zA-Z0-9._], consecutive dots rejected. Unique case-insensitively. Changeable once per 30 days.

  • Name
    display_name
    Type
    string
    Description

    1–32 chars, any Unicode. Defaults to the email local-part at signup. Subject to the impersonation blocklist.

  • Name
    role
    Type
    UserRole
    Description

    0 MEMBER, 1 ADMIN, 2 OWNER.

  • Name
    bio
    Type
    string | null
    Description

    Up to 1024 chars.

  • Name
    avatar
    Type
    string | null
    Description

    sha256[0:16] content hash. URL is ${CDN_BASE_URL}/avatars/${user.id}/${avatar}.png.

  • Name
    verified_at
    Type
    string | null
    Description

    Timestamp the user completed email verification. Null until then.

  • Name
    suspended_until
    Type
    string | null
    Description

    If set and in the future, the user is suspended.

  • Name
    disabled_at
    Type
    string | null
    Description

    Hard disable — only reactivation flow reachable.

  • Name
    deleted_at
    Type
    string | null
    Description

    Soft delete. Row preserved so message authorship doesn't dangle.

  • Name
    created_at
    Type
    string
    Description

    ISO-8601.

  • Name
    updated_at
    Type
    string
    Description

    ISO-8601, maintained by a DB trigger.

  • Name
    premium_type
    Type
    integer
    Description

    0 none. Higher tiers gate badges + cosmetics.

  • Name
    oid
    Type
    string | null
    Description

    Turnkey sub-organization id. Null for the wormhole service principal.

  • Name
    wallet_solana, wallet_ethereum, wallet_bitcoin, wallet_zcash
    Type
    string | null
    Description

    Multi-chain addresses provisioned at signup.

The derived "active" check is:

const isActive = (u: User) =>
  u.verified_at !== null
  && u.deleted_at === null
  && u.disabled_at === null
  && (u.suspended_until === null || new Date(u.suspended_until) < new Date());

Most gates that should refuse banned or locked users route through this so the rule stays in one place.


Get the current user

Returns the full User shape for the bearer token holder. The most common single endpoint in the entire API — clients hit this on every cold start to hydrate state.

Request

GET
/v1/users/@me
curl https://api.localuniverse.io/v1/users/@me \
  -H "authorization: <token>"

Update the current user

PATCH semantics: omitted = leave alone, value = set, null = clear (only on nullable columns).

color_item_id is a transient onboarding-only field — when present, the server grants that color item to the user and equips it to the SKIN slot inside the same transaction as the profile update. It is not a column.

  • Name
    username
    Type
    string
    Description

    Subject to the once-per-30-days update cap.

  • Name
    display_name
    Type
    string
    Description

    1–32 chars Unicode.

  • Name
    bio
    Type
    ?string
    Description

    Pass null to clear.

  • Name
    avatar
    Type
    ?string
    Description

    sha256[0:16] hash from /users/@me/avatar. null to clear.

  • Name
    color_item_id
    Type
    string
    Description

    Onboarding-only: grant + equip this color item to SKIN.

Request

PATCH
/v1/users/@me
curl -X PATCH https://api.localuniverse.io/v1/users/@me \
  -H "authorization: <token>" \
  -H "content-type: application/json" \
  -d '{ "display_name": "Tino", "bio": "Building" }'

Upload an avatar

Multipart upload of a PNG. Server validates magic-bytes, hashes (sha256[0:16]), stores at avatars/<user_id>/<hash>.png in CDN_BUCKET, stamps the new hash onto users.avatar, and returns the refreshed user row.

PNG only. 2 MB cap. Identical bytes for the same user dedupe naturally — same hash → same key.

Request

POST
/v1/users/@me/avatar
curl -X POST https://api.localuniverse.io/v1/users/@me/avatar \
  -H "authorization: <token>" \
  -F "file=@./avatar.png"

Settings

Client-defined JSON blob (up to 64 KB) the API stores opaquely. The server never parses it — it's an arbitrary key-value bag the app uses for UI prefs.

MethodPathNotes
GET/v1/users/@me/settingsReturns the latest blob + updated_at.
PATCH/v1/users/@me/settingsReplaces the whole blob. Body shape: { "settings": "<stringified JSON>" }.

Channels

Channels under /@me are the caller's inbox view — DMs they're in, group DMs, and HUBs they have membership in.

MethodPathNotes
GET/v1/users/@me/channelsList the caller's channels.
POST/v1/users/@me/channelsOpen a DM or group DM. Body discriminates: { recipient_id } for DM, { recipient_ids: [...] } for group DM.

HUB / HOME creation lives on POST /v1/channels instead — see Channels.


Inventory

Owned ItemInstance rows.

MethodPathNotes
GET/v1/users/@me/inventoryList the caller's instances.
GET/v1/users/@me/inventory/:instanceIdFetch a single instance.
DELETE/v1/users/@me/inventory/:instanceIdDiscard an instance the caller owns.
POST/v1/users/:userId/inventoryAdmin grant. Requires items.write.

Gameplay drops (fish caught, NPC reward) call itemInstanceService.grant() directly from their own service — they don't round-trip through this HTTP surface.


Equipment

MethodPathNotes
GET/v1/users/@me/equipmentCurrent equipped instances by slot.
PUT/v1/users/@me/equipment/:slotEquip an instance into a slot. Body: { instance_id }.
DELETE/v1/users/@me/equipment/:slotClear a slot.
PATCH/v1/users/:userId/equipmentBulk update on someone else's behalf. Requires equipment.write.

The slot is an integer in the URL — see EquipmentSlot in @localuniverse/common/types for the enum.


Sessions

See Authentication → Sessions — every active session, plus revoke endpoints.


Relationships

Friends, friend requests, and blocks live under /@me/relationships.

MethodPathNotes
GET/v1/users/@me/relationshipsList every relationship row for the caller.
POST/v1/users/@me/relationshipsSend a friend request. Body: { target_id } or { username }.
PATCH/v1/users/@me/relationships/:targetIdAccept / change relationship type.
DELETE/v1/users/@me/relationships/:targetIdRemove the relationship (unfriend or cancel a pending request).
PUT/v1/users/@me/blocks/:targetIdBlock.
DELETE/v1/users/@me/blocks/:targetIdUnblock.

The type field follows RelationshipType in @localuniverse/common/types: 1 FRIEND, 2 OUTGOING_FRIEND_REQUEST, 3 INCOMING_FRIEND_REQUEST, 4 OUTGOING_BLOCK, 5 INCOMING_BLOCK.


Other users

PublicUser

Returned by GET /v1/users/:userId, GET /v1/users?q=..., and embedded inside channel recipient lists. A strict subset of the full user row — no email, no lifecycle timestamps.

  • Name
    id
    Type
    string
    Description
  • Name
    username
    Type
    string | null
    Description
  • Name
    display_name
    Type
    string
    Description
  • Name
    role
    Type
    UserRole
    Description
  • Name
    bio
    Type
    string | null
    Description
  • Name
    avatar
    Type
    string | null
    Description
  • Name
    created_at
    Type
    string
    Description
  • Name
    public_flags
    Type
    integer
    Description
  • Name
    premium_type
    Type
    integer
    Description
  • Name
    bot
    Type
    boolean
    Description

Endpoints

MethodPathPermissionNotes
GET/v1/usersusers.readSearch by username prefix. ?q=<prefix>&limit=<n> (max 50). Only rows with a non-null username appear.
GET/v1/users/:userIdusers.readFull row, but with private fields elided to PublicUser when the caller isn't an admin.
GET/v1/users/:userId/eventsanalytics.readPer-user event feed for moderation. Keyset-paginated by event id: ?before=<id>&limit=<n>.

Was this page helpful?