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
nullor 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:
| Status | Reason |
|---|---|
| 400 | Invalid email format or inviting yourself |
| 409 | Pending 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
| Trigger | Title | Body | data.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
| Status | When |
|---|---|
| 400 | Invalid input (bad email, nickname too long, self-invite, self-challenge) |
| 401 | Missing, expired, or invalid Clerk JWT |
| 403 | Action not permitted for this user (wrong invitee, not a friend, etc.) |
| 404 | Resource not found or user not a participant |
| 409 | Conflict — duplicate pending record or already responded |