Esposter

Likes

Reddit-style voting: each user holds at most one like per post with value ∈ {1, −1}, shown as up/down arrows on every post and comment card beside the net count.

How it works

Modellikes(userId, postId, value) with a composite primary key (one row per user per post) and a DB check constraining value to ±1. The post's noLikes is the denormalized net sum.

Mutations — three procedures, each a transaction that writes the like row, adjusts noLikes, and recomputes the stored ranking from the new count:

  • createLike — first vote (noLikes += value).
  • updateLike — flip an existing vote (noLikes += 2 × value, rejecting no-op flips).
  • deleteLike — retract (noLikes −= value).

Viewer-scoped reads — every procedure that returns a post (reads and mutations alike) carries viewerLike: Like | undefined on PostWithRelations: at most the viewer's own like row, never the full list, since the net count already lives in the denormalized noLikes. The likes relation is only the server-side fetch strategy — getViewerPostRelations filters it to the caller, and getPostWithViewerLike maps the result. Unauthenticated rate-limited reads have no viewer, so they skip the like lookup entirely and the arrows render uncolored. A hot feed page's payload is O(posts) instead of O(total likes).

ClientLikeSection.vue derives liked/unliked from viewerLike, and maps arrow clicks to the right mutation (up while unliked = flip, up while liked = retract, …). useLikeOperations applies the result optimistically to whichever store owns the list (feed posts vs a post page's comments — two store instances of the same shape), patching viewerLike and noLikes in place so counts update without a refetch.

Procedures

ProcedureAuthInputPurpose
like.createLikeauthedpostId, value(±1)first vote
like.updateLikeauthedpostId, value(±1)flip vote
like.deleteLikeauthedpostIdretract vote

Key files

Paths relative to packages/app, except those starting with packages/, which are relative to the repo root.

FileRole
packages/db-schema/src/schema/likes.tstable + ±1 check
packages/db-schema/src/relations/postsRelation.tsPostWithRelations + viewerLike
server/trpc/routers/like.tstransactional mutations
server/services/post/getViewerPostRelations.tsviewer-filtered likes fetch strategy
server/services/post/getPostWithViewerLike.tsmaps the fetched row to viewerLike
app/components/Post/LikeSection.vuearrows UI + state derivation
app/composables/post/useLikeOperations.tsoptimistic store patching
app/store/post/like.ts, app/store/post/comment/like.tsper-list like stores

Notes

  • Like achievements (LikeAchievementDefinitionMap) trigger on these procedure paths like all achievement definitions.