Gifts — Technical Reference
Where this lives
Backend
apps/backend/graphql/types/gift.type.js—GiftCatalogItem,Gift,GiftTargetStats,SendGiftInput,SendGiftResponse; queriesgiftCatalog/giftStatsForTarget/giftsForTarget; mutationsendGift.apps/backend/graphql/resolvers/gift.resolver.js— wires the above to the manager.apps/backend/managers/coin-managers/gift.manager.js— catalog, balance check, coin transfer, stats.apps/backend/database/models/Gift.js+ migration20260721000000-create-gift.js— thegifttable.apps/backend/data-access-services/gift/gift.access-service.js—Giftrow persistence + theGROUP BY gift_typeaggregation behindgetStatsForTarget.apps/backend/graphql/types/post.type.js/post.resolver.js—Post.giftStats: GiftTargetStats!is resolved per-post viagift.manager#getStatsForTarget, so the feed query gets gift totals bundled onto each post instead of a separategiftStatsForTargetround-trip per card.
Live gifts are a separate system (live-stream-gift.type.js / live-stream-gift.resolver.js / LiveStreamGift.js, mutation sendLiveGift); don't confuse the two.
Frontend
apps/frontend-nextjs/src/components/gifts/GiftPicker.tsx— the sticker picker (loadsgiftCatalog, callssendGift).apps/frontend-nextjs/src/components/gifts/GiftsModal.tsx— per-target received-gifts view (giftStatsForTarget/giftsForTarget).- Entry points:
components/chat/ChatView.tsx(1:1 chat header gift button),components/PostCard.tsx,components/stories/StoryViewer.tsx, andpage-components/LiveRoomPage.tsx(live variant).
GraphQL
type GiftCatalogItem { giftType: String! label: String! emoji: String! coinAmount: Int! }
input SendGiftInput {
"One of: post, story, clip, user."
targetType: String!
targetId: ID!
giftType: String!
message: String
}
type SendGiftResponse {
success: Boolean!
message: String # error detail when success is false (e.g. insufficient balance)
giftType: String
emoji: String
coinAmount: Int
newBalance: Int # sender's coin balance after the gift
}
extend type Query {
giftCatalog: [GiftCatalogItem!]!
giftStatsForTarget(targetType: String!, targetId: ID!): GiftTargetStats!
giftsForTarget(targetType: String!, targetId: ID!, limit: Int, offset: Int): [Gift!]!
}
extend type Mutation {
sendGift(input: SendGiftInput!): SendGiftResponse!
}
GiftTargetStats breaks down accumulated gifts on a target as byType: [GiftTargetStatItem!] (per sticker: count, totalCoins) plus totalCoins / totalGifts.
Notes
- Pricing is the catalog
coinAmount; the manager verifies the sender's balance before transferring, sosendGiftreturnssuccess:falsewith amessagerather than throwing on insufficient funds. sendGiftalso creates agift-type notification for the recipient and returns the sender's post-gift balance (newBalance, viacoinTransactionManager#getUserBalance); both are best-effort and never fail the gift itself.myGiftablePostsis not part of this system despite the name — it lives intop-fans.type.js/top-fans.resolver.js/top-fans.manager.jsand lists a creator's paid posts offered as arewardTopFanreward (premium_post), used only byRewardFanModal.tsx. See the Top Fans reference.