Skip to content

Social API — Friends, Users, Challenges

All endpoints require Authorization: Bearer <Clerk JWT>.


Users

GET /users/me

Returns the authenticated user's profile from Clerk.

json
{
  "id": "user_abc123",
  "firstName": "Anton",
  "lastName": null,
  "imageUrl": "https://...",
  "publicMetadata": {
    "nickname": "Tono"
  }
}

No database reads — Clerk only.

PATCH /users/me

Update the user's nickname.

Body:

json
{ "nickname": "Tono" }
  • Max 64 characters
  • null or empty string removes the nickname
  • Whitespace trimmed; empty-after-trim is rejected
  • No database writes — Clerk metadata update only

Response: Same shape as GET /users/me.


Friends

Base path: /friends

POST /friends/invitations

Send a friend invitation by email address.

Body:

json
{ "email": "friend@example.com" }

Response 201:

json
{
  "id": "inv_xyz",
  "senderId": "user_abc",
  "inviteeEmail": "friend@example.com",
  "status": "pending",
  "createdAt": "...",
  "updatedAt": "..."
}

Errors:

StatusReason
400Invalid email format or inviting yourself
409Pending invitation to this email already exists

Side effects:

  • Emits friendRequestReceived { invitationId, senderId, senderName } to the invitee via WebSocket if online
  • Sends push notification as fallback

GET /friends/invitations

List pending incoming and outgoing invitations.

json
{
  "incoming": [ { "id": "...", "senderId": "...", "inviteeEmail": "...", "status": "pending", ... } ],
  "outgoing": [ { ... } ]
}

Both lists contain only status = "pending" records.

POST /friends/invitations/:id/accept

Accept a pending invitation. Only the invitee (email match) can accept.

  • Creates a bidirectional friendship record
  • Returns updated invitation with status: "accepted"

Errors: 403 (not invitee), 404 (not found), 409 (already responded)

POST /friends/invitations/:id/decline

Decline a pending invitation. Only the invitee can decline. No friendship created.

Errors: 403, 404, 409

DELETE /friends/invitations/:id

Cancel an outgoing invitation. Only the sender can cancel, and only while status = "pending".

Response: { "ok": true }

Errors: 403 (not sender), 404, 409 (not pending)

GET /friends

List accepted friends.

json
{
  "friends": [
    { "id": "user_def", "name": "Alex" }
  ]
}

Display name resolution order: nickname (Clerk public metadata) → firstName → primary email → userId.


Challenges

Base path: /challenges

GET /challenges

List incoming and outgoing challenges.

json
{
  "outgoing": [
    {
      "id": "chal_abc",
      "challengerId": "user_abc",
      "recipientId": "user_def",
      "status": "pending",
      "matchId": null,
      "createdAt": "...",
      "updatedAt": "..."
    }
  ],
  "incoming": [ { ... } ]
}

Challenge statuses: pending | accepted | declined | cancelled | expired

Challenge creation, acceptance, decline, and cancellation happen via WebSocket (see API Reference — Matchmaking Gateway). REST is read-only for challenges.

Business rules for challenge creation (enforced server-side on the socket event):

  • Can only challenge friends (403 otherwise)
  • Only one pending challenge per (challenger, recipient) pair (409 otherwise)
  • Cannot challenge yourself (400)

Push Notifications

No REST endpoints. The push module is internal-only.

Token registration

Expo push tokens are registered as part of the identify WebSocket event:

text
Client emits: identify { playerId, name, expoPushToken }

The server stores the token in the pushTokens table (upsert — idempotent).

Token format: Must match ExponentPushToken[...] — silently ignored otherwise.

When push notifications fire

TriggerTitleBodydata.type
Opponent played a move"Your turn""{name} played a band"match_move
Friend request received"Friend request""{name} sent you a friend request"friend_request
Challenge received(sent via socket primarily)

Push is suppressed for a user if their app is in the foreground (presenceService.isForeground(userId) === true).

Tokens reported as DeviceNotRegistered by Expo are automatically removed from the database.


Error Reference

StatusWhen
400Invalid input (bad email, nickname too long, self-invite, self-challenge)
401Missing, expired, or invalid Clerk JWT
403Action not permitted for this user (wrong invitee, not a friend, etc.)
404Resource not found or user not a participant
409Conflict — duplicate pending record or already responded