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.
User ID of the target user
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.
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.
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>
Show FriendWithDetails Object
Friend’s last activity timestamp
Whether you sent the friend request
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)
Show ActivityWithUser Object
Friend who performed the activity
Activity type (e.g., “game_completed”, “level_up”, “badge_earned”)
XP earned from this activity
When the activity occurred
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>
Show NotificationWithSender Object
type
'revenge' | 'friend_request' | 'battle_invite'
Notification type
Type-specific notification data
Whether notification has been read (always false in results)
Name of the user who triggered the notification
Notification Types
revenge - Someone beat your score
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!"
}
friend_request - Incoming friend request
Sent when someone sends you a friend request.Data fields:{
senderId: Id<"users">
}
battle_invite - Live battle invitation
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:
First user in the relationship
Second user in the relationship
User who sent the friend request
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>
);
}