Skip to main content

Overview

TrackGeek provides a comprehensive social platform that enables users to connect, interact, and share their media experiences. The social system includes user relationships, contextual comments, emoji reactions, and activity feeds.

User Relationships

Following System

TrackGeek uses a unidirectional following model where users can follow others without requiring mutual approval. Following Model (prisma/schema.prisma:149-162):
model Following {
  id          String   @id @default(uuid())
  followerId  String
  followingId String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  follower  User @relation("UserFollowers", fields: [followerId], references: [id], onDelete: Cascade)
  following User @relation("UserFollowing", fields: [followingId], references: [id], onDelete: Cascade)

  @@unique([followerId, followingId])
  @@index([followerId])
  @@index([followingId])
}

Relationship Terminology

Follower

A user who follows another user. They see the followed user’s activity in their feed.

Following

A user being followed. Their activity appears in their followers’ feeds.

Unique Constraints

The @@unique([followerId, followingId]) constraint ensures:
  • No duplicate follow relationships
  • One follow record per user pair
  • Clean unfollowing (simple delete)

Indexing Strategy

@@index([followerId]) enables efficient queries:
  • “Who is this user following?”
  • “Get all users followed by X”
  • Feed generation (find followed users’ activity)
@@index([followingId]) enables efficient queries:
  • “Who follows this user?”
  • “Get all followers of X”
  • Follower count aggregation

User Model Relations

User Relationship Fields (prisma/schema.prisma:34-35):
followers  Following[] @relation("UserFollowers")
following  Following[] @relation("UserFollowing")
This bidirectional relation allows querying from both perspectives:
// Get users I follow
const following = await prisma.user.findUnique({
  where: { id: userId },
  include: { following: true }
});

// Get my followers
const followers = await prisma.user.findUnique({
  where: { id: userId },
  include: { followers: true }
});

Comment System

Comment Types

Comments can be attached to any media type or user profile: CommentType Enum (prisma/schema.prisma:164-172):
enum CommentType {
  Anime
  Manga
  TVShow
  Movie
  Game
  Book
  Profile
}

Comment Model

Comment Schema (prisma/schema.prisma:174-206):
model Comment {
  id        String      @id @default(uuid())
  content   String
  type      CommentType
  userId    String
  animeId   String?
  mangaId   String?
  tvShowId  String?
  movieId   String?
  gameId    String?
  bookId    String?
  profileId String?
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  anime     Anime?     @relation(fields: [animeId], references: [id], onDelete: Cascade)
  manga     Manga?     @relation(fields: [mangaId], references: [id], onDelete: Cascade)
  tvShow    TvShow?    @relation(fields: [tvShowId], references: [id], onDelete: Cascade)
  movie     Movie?     @relation(fields: [movieId], references: [id], onDelete: Cascade)
  game      Game?      @relation(fields: [gameId], references: [id], onDelete: Cascade)
  book      Book?      @relation(fields: [bookId], references: [id], onDelete: Cascade)
  profile   Profile?   @relation(fields: [profileId], references: [id], onDelete: Cascade)
  reactions Reaction[]

  @@unique([userId, animeId])
  @@unique([userId, mangaId])
  @@unique([userId, tvShowId])
  @@unique([userId, movieId])
  @@unique([userId, gameId])
  @@unique([userId, bookId])
  @@unique([userId, profileId])
}

Polymorphic Association

Comments use a polymorphic pattern with:
  • type field indicating the comment target
  • Optional foreign keys for each media type
  • Exactly one foreign key populated per comment
The multiple unique constraints ensure each user can only leave one comment per media item or profile.

Comment Creation

Example from CommentService (src/modules/comment/comment.service.ts:14-27):
async createComment(createCommentDto: CreateCommentDto) {
  const { type, content, userId, item } = createCommentDto;

  const entityId = { ...item } as Record<string, any>;

  await this.databaseService.comment.create({
    data: {
      type,
      content,
      userId,
      ...entityId,
    },
  });
}
The item object contains the appropriate foreign key:
// For anime comment
{ animeId: "uuid" }

// For profile comment
{ profileId: "uuid" }

Comment Retrieval

Example from CommentService (src/modules/comment/comment.service.ts:43-132):
async getComments(getCommentsDto: GetCommentsDto) {
  const pagination = await this.databaseService.cursorPagination<CommentFindManyArgs>({
    model: "comment",
    where: {
      type: getCommentsDto.type,
      ...getCommentsDto.item,
    },
    cursor: getCommentsDto.cursor,
    itemsPerPage: getCommentsDto.itemsPerPage,
    include: {
      user: {
        select: {
          id: true,
          name: true,
          username: true,
          profile: {
            select: {
              id: true,
              avatarUrl: true,
            },
          },
        },
      },
      anime: {
        select: {
          id: true,
          malId: true,
          title: true,
          imageUrl: true,
        },
      },
      manga: {
        select: {
          id: true,
          malId: true,
          title: true,
          imageUrl: true,
        },
      },
      tvShow: {
        select: {
          id: true,
          tmdbId: true,
          name: true,
          posterUrl: true,
        },
      },
      movie: {
        select: {
          id: true,
          tmdbId: true,
          title: true,
          posterUrl: true,
        },
      },
      game: {
        select: {
          id: true,
          igdbId: true,
          name: true,
          coverUrl: true,
        },
      },
      book: {
        select: {
          id: true,
          hardcoverId: true,
          title: true,
          imageUrl: true,
        },
      },
      reactions: {
        take: 3,
        orderBy: { createdAt: "desc" },
        select: {
          id: true,
          emoji: true,
          createdAt: true,
          user: {
            select: {
              username: true,
            },
          },
        },
      },
    },
  });

  return pagination;
}
Comments include reactions preview (first 3) for efficient rendering without additional queries.

Comment Deletion

Example from CommentService (src/modules/comment/comment.service.ts:29-41):
async deleteComment(deleteCommentDto: DeleteCommentDto) {
  const comment = await this.databaseService.comment.findUnique({
    where: { id: deleteCommentDto.commentId },
  });

  if (!comment || comment.userId !== deleteCommentDto.userId) {
    throw new AppException(ERROR_CODES.COMMENT_NOT_FOUND);
  }

  await this.databaseService.comment.delete({
    where: { id: comment.id },
  });
}
Deletion requires user authorization - only comment authors can delete their comments.

Reaction System

Reaction Types

Reactions can be added to comments or feed events: ReactionType Enum (prisma/schema.prisma:208-211):
enum ReactionType {
  Comment
  FeedEvent
}

Reaction Model

Reaction Schema (prisma/schema.prisma:213-228):
model Reaction {
  id          String       @id @default(uuid())
  emoji       String
  type        ReactionType
  userId      String
  commentId   String?
  feedEventId String?
  createdAt   DateTime     @default(now())

  user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  comment   Comment?   @relation(fields: [commentId], references: [id], onDelete: Cascade)
  feedEvent FeedEvent? @relation(fields: [feedEventId], references: [id], onDelete: Cascade)

  @@unique([userId, commentId])
  @@unique([userId, feedEventId])
}

Emoji Reactions

Reactions use emoji strings for flexible expression:
emoji: "👍" | "❤️" | "😂" | "😮" | "😢" | "🔥" | ...
  • 👍 Like/Approve
  • ❤️ Love
  • 😂 Funny
  • 😮 Surprised
  • 😢 Sad
  • 🔥 Fire/Awesome
  • 🎉 Celebrate
  • 👏 Applause
  • 💯 Perfect
  • 🤔 Thinking

Unique Constraints

Reactions enforce one emoji per user per target:
  • @@unique([userId, commentId]): One reaction per user per comment
  • @@unique([userId, feedEventId]): One reaction per user per feed event
Users can change their reaction by deleting and creating a new one, but cannot have multiple reactions on the same target.

Reaction Creation

Example from ReactionService (src/modules/reaction/reaction.service.ts:14-27):
async createReaction(createReactionDto: CreateReactionDto) {
  const { emoji, userId, item, type } = createReactionDto;

  const entityId = { ...item } as Record<string, any>;

  await this.databaseService.reaction.create({
    data: {
      type,
      emoji,
      userId,
      ...entityId,
    },
  });
}

Reaction Retrieval

Example from ReactionService (src/modules/reaction/reaction.service.ts:43-72):
async getReactions(getReactionsDto: GetReactionsDto) {
  const pagination = await this.databaseService.cursorPagination<ReactionFindManyArgs>({
    model: "reaction",
    where: {
      type: getReactionsDto.type,
      ...getReactionsDto.item,
    },
    cursor: getReactionsDto.cursor,
    itemsPerPage: getReactionsDto.itemsPerPage,
    include: {
      comment: true,
      feedEvent: true,
      user: {
        select: {
          id: true,
          name: true,
          username: true,
          profile: {
            select: {
              id: true,
              avatarUrl: true,
            },
          },
        },
      },
    },
  });

  return pagination;
}

Reaction Deletion

Example from ReactionService (src/modules/reaction/reaction.service.ts:29-41):
async deleteReaction(deleteReactionDto: DeleteReactionDto) {
  const reaction = await this.databaseService.reaction.findUnique({
    where: { id: deleteReactionDto.reactionId },
  });

  if (!reaction || reaction.userId !== deleteReactionDto.userId) {
    throw new AppException(ERROR_CODES.REACTION_NOT_FOUND);
  }

  await this.databaseService.reaction.delete({
    where: { id: reaction.id },
  });
}

Reaction Aggregation

Reactions can be grouped by emoji for display:
// Example aggregation
{
  "👍": { count: 15, users: ["user1", "user2", ...] },
  "❤️": { count: 8, users: ["user3", "user4", ...] },
  "😂": { count: 3, users: ["user5"] }
}

Feed System

Feed Event Types

Feed events track various user activities: FeedEventType Enum (prisma/schema.prisma:230-238):
enum FeedEventType {
  NewFollower
  NewFavorite
  NewList
  NewListItem
  NewReview
  NewWatch
  NewProgress
}
  • NewFollower: User gained a new follower
  • NewFavorite: User marked media as favorite

Feed Event Model

FeedEvent Schema (prisma/schema.prisma:240-251):
model FeedEvent {
  id        String        @id @default(uuid())
  type      FeedEventType
  userId    String
  metadata  Json?
  createdAt DateTime      @default(now())

  user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  reactions Reaction[]

  @@index([userId, createdAt])
}

Metadata Field

The metadata JSON field stores event-specific data:
{
  "animeReview": {
    "id": "uuid",
    "overall": 8.5,
    "summary": "Great anime!",
    "anime": {
      "id": "uuid",
      "title": "Attack on Titan",
      "imageUrl": "https://..."
    },
    "user": {
      "id": "uuid",
      "name": "John Doe",
      "username": "johndoe"
    }
  }
}
{
  "progress": {
    "id": "uuid",
    "status": "Watching",
    "anime": {
      "id": "uuid",
      "title": "Demon Slayer",
      "imageUrl": "https://..."
    }
  }
}
{
  "follower": {
    "id": "uuid",
    "name": "Jane Smith",
    "username": "janesmith",
    "profile": {
      "avatarUrl": "https://..."
    }
  }
}

Feed Event Creation

Example from FeedEventService (src/modules/feed-event/feed-event.service.ts:14-24):
async createFeedEvent(feedEventDto: FeedEventDto) {
  const { type, userId, metadata } = feedEventDto;

  await this.databaseService.feedEvent.create({
    data: {
      type,
      userId,
      metadata,
    },
  });
}
Feed events are typically created asynchronously via a queue system to avoid blocking user operations.

Personalized Feed Generation

Example from FeedEventService (src/modules/feed-event/feed-event.service.ts:26-76):
async getFeedEventsByUserId(getFeedEventsByUserIdDto: GetFeedEventsByUserDto) {
  const userExists = await this.databaseService.user.findUnique({
    where: { id: getFeedEventsByUserIdDto.userId },
  });

  if (!userExists) {
    throw new AppException(ERROR_CODES.USER_NOT_FOUND);
  }

  const following = await this.databaseService.following.findMany({
    where: { followerId: getFeedEventsByUserIdDto.userId },
    select: { followingId: true },
  });

  const friendIds = following.map((item) => item.followingId);

  const pagination = await this.databaseService.cursorPagination<FeedEventFindManyArgs>({
    model: "feedEvent",
    cursor: getFeedEventsByUserIdDto.cursor,
    itemsPerPage: getFeedEventsByUserIdDto.itemsPerPage,
    where: {
      userId: {
        in: [...friendIds, getFeedEventsByUserIdDto.userId],
      },
    },
    include: {
      _count: {
        select: {
          reactions: true,
        },
      },
      reactions: {
        take: 3,
        orderBy: { createdAt: "desc" },
        select: {
          id: true,
          emoji: true,
          createdAt: true,
          user: {
            select: {
              username: true,
            },
          },
        },
      },
    },
    orderBy: { createdAt: "desc" },
  });

  return pagination;
}

Feed Algorithm

  1. Get Following List: Query users the current user follows
  2. Include Self: Add current user’s ID to include their own activity
  3. Filter Events: Get feed events where userId IN (following + self)
  4. Sort by Time: Order by createdAt descending (newest first)
  5. Include Reactions: Preload reaction counts and preview
  6. Paginate: Return cursor-based pagination

Feed Indexing

Composite Index (prisma/schema.prisma:250):
@@index([userId, createdAt])
This composite index optimizes:
  • Filtering by user ID
  • Sorting by creation time
  • Range queries (get events after timestamp)

Social Analytics

The social system enables rich analytics:

Follower Count

Total followers per user

Following Count

Total users followed

Comment Count

Total comments per media/user

Reaction Count

Total reactions per comment/event

Engagement Rate

Reactions + comments per feed event

Popular Content

Media with most comments/reactions

Active Users

Users with most feed events

Network Size

Follower + following counts

Feed Activity

Events per day/week/month

Best Practices

Cascade Deletes

All social relationships cascade delete when users are removed

Authorization

Always verify user ownership before deletion

Pagination

Use cursor-based pagination for feeds and comments

Reaction Preview

Include reaction previews to reduce client queries

Async Events

Create feed events asynchronously via queue

Index Optimization

Leverage composite indexes for feed queries

Build docs developers (and LLMs) love