Skip to main content

Overview

The social system enables players to connect with friends, view activity feeds, and receive notifications for battles and achievements.
Guest users cannot access social features. Users with isAnonymous: true will receive error messages prompting them to sign up.

Friends System

sendFriendRequest

Send a friend request to another user.
targetId
Id<'users'>
User ID of the target user
targetEmail
string
Email address of the target user
Provide either targetId or targetEmail (not both).
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

const sendRequest = useMutation(api.social.sendFriendRequest);

const result = await sendRequest({ targetId: "k123abc" });
// or
const result = await sendRequest({ targetEmail: "[email protected]" });

if (result.error) {
  console.error(result.error);
}
Returns: { success: true } | { error: string }

Error Cases

  • "Guest users cannot access social features. Please sign up." - Anonymous user
  • "User not found" - Target user doesn’t exist
  • "Cannot add yourself" - Trying to friend yourself
  • "Request already sent or friends" - Duplicate request

acceptFriendRequest

Accept a pending friend request.
friendshipId
Id<'friends'>
required
ID of the friendship record to accept
const acceptRequest = useMutation(api.social.acceptFriendRequest);

await acceptRequest({ friendshipId: "k456def" });
Returns: { success: true } | { error: string } Updates the friendship status from pending to active.

rejectFriendRequest

Reject or remove a friend connection.
friendshipId
Id<'friends'>
required
ID of the friendship record to reject/remove
const rejectRequest = useMutation(api.social.rejectFriendRequest);

await rejectRequest({ friendshipId: "k456def" });
Returns: { success: true } | { error: string } Deletes the friendship record entirely.

getFriends

Get all friend connections for the current user.
const friends = useQuery(api.social.getFriends);
Returns: Array<FriendWithDetails> Returns all friendships (both pending and active).

getFriendsActivity

Get recent activity from friends.
const activity = useQuery(api.social.getFriendsActivity);
Returns: Array<ActivityWithUser> (max 10 most recent)
Only returns activities from users with active friend status.

Notifications

getNotifications

Get all unread notifications for the current user.
const notifications = useQuery(api.social.getNotifications);
Returns: Array<NotificationWithSender>

Notification Types

Sent when another player beats your ghost score in a battle.Data fields:
{
  senderId: Id<"users">,
  gameId: string,
  battleId: Id<"battles">,
  amount: number, // Points difference
  message: string // e.g., "beat your score by 150 points!"
}
Sent when someone sends you a friend request.Data fields:
{
  senderId: Id<"users">
}
Sent when someone challenges you to a live battle.Data fields:
{
  senderId: Id<"users">,
  gameId: string,
  battleId: Id<"battles">
}

markRead

Mark a notification as read.
notificationId
Id<'notifications'>
required
Notification ID to mark as read
const markRead = useMutation(api.social.markRead);

await markRead({ notificationId: "k789ghi" });
Updates read: false to read: true. The notification will no longer appear in getNotifications results.

Friendship Schema

Friendships are stored bidirectionally in the friends table: Indexes:
  • by_user1 - Query friendships where user is user1
  • by_user2 - Query friendships where user is user2
  • by_pair - Check if friendship exists between two users
  • by_status - Filter by pending/active status

Example: Social Dashboard

import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

function SocialDashboard() {
  const user = useQuery(api.users.viewer);
  const friends = useQuery(api.social.getFriends);
  const activity = useQuery(api.social.getFriendsActivity);
  const notifications = useQuery(api.social.getNotifications);
  
  const acceptRequest = useMutation(api.social.acceptFriendRequest);
  const rejectRequest = useMutation(api.social.rejectFriendRequest);
  const markRead = useMutation(api.social.markRead);
  
  if (user?.isAnonymous) {
    return (
      <div>
        <h2>Social Features Locked</h2>
        <p>Sign up to add friends and compete in battles!</p>
      </div>
    );
  }
  
  const pendingRequests = friends?.filter(
    f => f.status === "pending" && !f.initiatedByMe
  );
  
  const activeFriends = friends?.filter(f => f.status === "active");
  
  return (
    <div>
      {/* Notifications */}
      <section>
        <h2>Notifications ({notifications?.length || 0})</h2>
        {notifications?.map(notif => (
          <div key={notif._id}>
            <img src={notif.senderImage} alt={notif.senderName} />
            <p>
              <strong>{notif.senderName}</strong>
              {notif.type === "revenge" && (
                <> {notif.data.message}</>
              )}
              {notif.type === "friend_request" && (
                <> sent you a friend request</>
              )}
            </p>
            <button onClick={() => markRead({ notificationId: notif._id })}>
              Dismiss
            </button>
          </div>
        ))}
      </section>
      
      {/* Pending Requests */}
      <section>
        <h2>Friend Requests ({pendingRequests?.length || 0})</h2>
        {pendingRequests?.map(friend => (
          <div key={friend._id}>
            <img src={friend.image} alt={friend.name} />
            <p>{friend.name}</p>
            <button onClick={() => acceptRequest({ friendshipId: friend._id })}>
              Accept
            </button>
            <button onClick={() => rejectRequest({ friendshipId: friend._id })}>
              Decline
            </button>
          </div>
        ))}
      </section>
      
      {/* Friends List */}
      <section>
        <h2>Friends ({activeFriends?.length || 0})</h2>
        {activeFriends?.map(friend => (
          <div key={friend._id}>
            <img src={friend.image} alt={friend.name} />
            <p>{friend.name}</p>
            <span>
              {friend.lastSeen && Date.now() - friend.lastSeen < 300000
                ? "Online"
                : "Offline"}
            </span>
          </div>
        ))}
      </section>
      
      {/* Activity Feed */}
      <section>
        <h2>Recent Activity</h2>
        {activity?.map(act => (
          <div key={act._id}>
            <img src={act.userImage} alt={act.userName} />
            <p>
              <strong>{act.userName}</strong> {act.type}
            </p>
            <span>+{act.xp} XP</span>
            <time>{new Date(act.timestamp).toLocaleString()}</time>
          </div>
        ))}
      </section>
    </div>
  );
}

Build docs developers (and LLMs) love