Creator Subscriptions — Technical Reference
← Back to Creator Subscriptions
Where this lives
Backend
apps/backend/graphql/resolvers/subscription-tier.resolver.js- tier CRUD and activation (createTier,activeCreatorTiers,myTiers, ...)apps/backend/graphql/resolvers/user-subscription.resolver.js- subscribing/cancelling/renewing (subscribe,mySubscriptions,isSubscribedTo,subscriberRetention, ...)apps/backend/graphql/resolvers/subscription-offer.resolver.js- free-trial/discount offer links (createSubscriptionOffer,offerByToken,redeemSubscriptionTrial,applicableDiscount, ...)apps/backend/graphql/types/subscription-tier.type.js-SubscriptionTierschemaapps/backend/graphql/types/user-subscription.type.js-UserSubscriptionschema,SubscriberRetentionPointapps/backend/graphql/types/subscription-offer.type.js-SubscriptionOffer,OfferPreview,ApplicableDiscountschemasapps/backend/managers/payment-managers/subscription-tier.manager.js- tier business logic, callssubscription-tier.access-service.jsapps/backend/managers/payment-managers/user-subscription.manager.js- subscribe/cancel/renew logic, coin deduction, andgetSubscriberRetentionanalytics, callsuser-subscription.access-service.jsapps/backend/managers/payment-managers/subscription-offer.manager.js- trial/discount offer CRUD, eligibility (audience: new|returning|all), redemption, and discount computation for bothprofileandgroupscopesapps/backend/data-access-services/payment/subscription-tier.access-service.js- tier DB accessapps/backend/data-access-services/payment/user-subscription.access-service.js- subscription record DB accessapps/backend/data-access-services/subscription-offer/subscription-offer.access-service.js- offer DB accessapps/backend/data-access-services/subscription-offer/subscription-offer-redemption.access-service.js- per-user redemption records (one per offer/user, used for eligibility checks)
Frontend
apps/frontend-nextjs/src/components/CreatePostModal.tsx- post visibility picker includes aSubscribersoption (PostVisibility.Subscribers) that gates a post behind the creator's subscriptionapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx- fetchesactiveCreatorTiers/isSubscribedToand renders the "Subscribe" call-to-action + tier picker on a creator's public profile, callingsubscribeapps/frontend-nextjs/src/page-components/settings/SubscriptionsSettingsPage.tsx-Settings → Subscriptions: creator tier management (createTier/updateTier/activateTier/deactivateTier/deleteTier) and the subscriber's own subscriptions list (mySubscriptions,cancelSubscription,renewSubscription)apps/frontend-nextjs/src/page-components/settings/SubscriptionOffersPage.tsx-Settings → Subscription offers: creator UI formyOffers/createSubscriptionOffer/deactivateSubscriptionOffercovering both profile tiers and paid groupsapps/frontend-nextjs/src/page-components/OfferRedemptionPage.tsx- public/offer/[token]landing page (offerByToken,redeemSubscriptionTrial), no auth required to previewapps/frontend-nextjs/src/hooks/useApplicableDiscount.tsandapps/frontend-nextjs/src/components/subscriptions/DiscountedPrice.tsx- show a discounted price on tier/group subscribe CTAs whenapplicableDiscountreturns oneapps/frontend-nextjs/src/page-components/settings/InsightsAndToolsPage.tsx- creator analytics screen, includes asubscriberRetentionchart alongsidesubscriberCountapps/frontend-nextjs/src/components/chat/hooks/useConversationSubscriptionAccess.ts- viewer-side paid-group access (hasConversationAccess,subscribeToConversation,cancelConversationSubscription), used byChatView.tsx/SubscriptionPaywall.tsxand by the paid-groups list onPublicProfilePage.tsx
Technical implementation checklist
-
createTier/updateTier/activateTier/deactivateTier/deleteTier— resolvers wired insubscription-tier.resolver.js; frontend tier management now lives onSettings → Subscriptions(SubscriptionsSettingsPage.tsx) -
activeCreatorTiers/subscribe— resolvers wired insubscription-tier.resolver.js/user-subscription.resolver.js; frontend now renders a tier picker and "Subscribe" call-to-action onPublicProfilePage.tsx, in addition to theSubscriberspost-visibility option inCreatePostModal.tsx -
mySubscriptions/cancelSubscription/renewSubscription/isSubscribedTo/subscriberCount— resolvers wired inuser-subscription.resolver.js; frontend wired viaSubscriptionsSettingsPage.tsx(list/cancel/renew) andPublicProfilePage.tsx(isSubscribedTo) -
mySubscribers— resolver wired inuser-subscription.resolver.js; no frontend screen found for the creator-side subscriber list -
subscriberRetention— resolver wired inuser-subscription.resolver.js, computed byuserSubscriptionManager.getSubscriberRetention(month-bucketed new/churned/active counts); frontend chart onInsightsAndToolsPage.tsx -
subscribeToConversation/hasConversationAccess/cancelConversationSubscription— resolvers wired inconversation-subscription.resolver.js; frontend wired viauseConversationSubscriptionAccess.ts→ChatView.tsx/SubscriptionPaywall.tsxand the paid-groups list onPublicProfilePage.tsx -
enableConversationSubscription/disableConversationSubscription— resolvers wired; frontend buttons inConversationDetailsPanel.tsx(handleEnableSubscription/handleDisableSubscription) let a group-chat creator turn paid access on/off -
conversationSubscriptionStats/mySubscriptionEarnings/conversationSubscribers/grantFreeConversationAccess— resolvers wired inconversation-subscription.resolver.js; no frontend usage found for any of these fields -
visibility: subscriberspost gating — the enum value is selectable inCreatePostModal.tsx, andpost.manager.js(apps/backend/managers/post-managers/post.manager.js, lines ~572-577) checksvisibility === 'subscribers'viauserSubscriptionManager.isSubscribed(...), throwing if the viewer isn't subscribed — enforced server-side - Subscription offers (
SubscriptionOffer) — free-trial and discount links for a profile tier or a paid group. Creator CRUD (createSubscriptionOffer/updateSubscriptionOffer/deactivateSubscriptionOffer/deleteSubscriptionOffer) wired inSubscriptionOffersPage.tsx(Settings → Subscription offers); public redemption (offerByToken/redeemSubscriptionTrial) wired inOfferRedemptionPage.tsxat/offer/[token];applicableDiscountwired viauseApplicableDiscount.ts/DiscountedPrice.tsxto show discounted prices on subscribe CTAs
Subscription tiers (SubscriptionTier)
| Field | Description |
|---|---|
name | Tier name (e.g. "Fan", "VIP") |
description | Benefits description |
coinPrice | Price in coins per period |
benefits | List of benefit strings |
isActive | Whether the tier is open for new subscribers |
subscriberCount | Current active subscribers |
activeCreatorTiers returns the tiers a creator has published and made available. Use this to render the "Subscribe" page on a creator's profile. myTiers returns the authenticated creator's own tiers — used on the creator settings screen.
createTier adds a new tier. The tier starts inactive; call activateTier to open it to subscribers. updateTier edits the name, description, price, or benefits of a tier. deactivateTier stops accepting new subscriptions but doesn't cancel existing ones. deleteTier permanently removes the tier — only possible if it has no active subscribers.
query CreatorTiers($creatorId: ID!) {
activeCreatorTiers(creatorId: $creatorId) {
id name description coinPrice benefits subscriberCount
}
}
query MyTiers { myTiers { id name subscriberCount } }
mutation CreateTier($input: SubscriptionTierCreateInput!) { createTier(input: $input) { id name coinPrice } }
mutation UpdateTier($tierId: ID!, $input: SubscriptionTierUpdateInput!) { updateTier(tierId: $tierId, input: $input) { id } }
mutation ActivateTier($tierId: ID!) { activateTier(tierId: $tierId) { isActive } }
mutation DeactivateTier($tierId: ID!) { deactivateTier(tierId: $tierId) { isActive } }
mutation DeleteTier($tierId: ID!) { deleteTier(tierId: $tierId) }
User subscriptions (UserSubscription)
mySubscriptions returns all creator subscriptions held by the current user — both active and cancelled. currentPeriodStart / currentPeriodEnd tell the client when the current billing cycle started and ends.
mySubscribers is the creator-side view: who is subscribed to you and on which tier.
isSubscribedTo is a lightweight boolean check — use it to gate exclusive content without fetching the full subscription object. subscriberCount returns a single integer for the creator's profile stats row.
subscribe creates a new subscription by paying the tier's coinPrice. The coins are deducted immediately. cancelSubscription marks the subscription cancelled — access continues until currentPeriodEnd. renewSubscription manually renews a cancelled or expired subscription.
enum SubscriptionStatus { active cancelled expired pending }
query MySubscriptions($status: SubscriptionStatus) {
mySubscriptions(status: $status) {
id status currentPeriodStart currentPeriodEnd
creator { username profilePicture }
tier { name coinPrice benefits }
}
}
query MySubscribers($status: SubscriptionStatus) {
mySubscribers(status: $status) {
id status subscriber { username }
tier { name }
}
}
# Lightweight boolean for gating exclusive content
query IsSubscribedTo($creatorId: ID!) { isSubscribedTo(creatorId: $creatorId) }
query SubscriberCount($creatorId: ID!) { subscriberCount(creatorId: $creatorId) }
# Start a subscription (deducts coinPrice from wallet immediately)
mutation Subscribe($input: UserSubscriptionCreateInput!) { subscribe(input: $input) { id status } }
# Cancel — access continues until end of current period
mutation CancelSubscription($subscriptionId: ID!) { cancelSubscription(subscriptionId: $subscriptionId) { status } }
# Renew a cancelled or expired subscription
mutation RenewSubscription($subscriptionId: ID!) { renewSubscription(subscriptionId: $subscriptionId) { status } }
subscriberRetention returns one SubscriberRetentionPoint per calendar month (default 6, max 24) with new/churned/active-at-end subscriber counts and churn/retention rates, for the authenticated creator's profile subscriptions:
query SubscriberRetention($months: Int) {
subscriberRetention(months: $months) {
month newSubscribers churned activeAtEnd churnRate retentionRate
}
}
Subscription offers (SubscriptionOffer)
A creator can generate a shareable link that grants either a free trial (kind: trial) or a discount on the first payment (kind: discount) for a profile tier (scope: profile) or a paid group conversation (scope: group). Offers are capped by maxRedemptions, can expire (expiresAt), and target an audience: new (never subscribed), returning (previously subscribed, now inactive), or all. Redemption is tracked per user so the same offer can't be redeemed twice.
mutation CreateSubscriptionOffer($input: CreateOfferInput!) {
createSubscriptionOffer(input: $input) { id token }
}
query MyOffers($scope: String, $kind: String) {
myOffers(scope: $scope, kind: $kind) {
id kind scope token name trialDays discountType discountValue
audience maxRedemptions redemptionCount expiresAt isActive
}
}
# Public preview — no auth required — rendered at /offer/<token>
query OfferByToken($token: String!) {
offerByToken(token: $token) {
kind scope name trialDays discountType discountValue
coinPrice isActive expired capReached redemptionsLeft
}
}
# Redeem a free-trial link (grants access that expires at the end of the trial, no auto-charge)
mutation RedeemSubscriptionTrial($token: String!) {
redeemSubscriptionTrial(token: $token) { success scope trialEndsAt }
}
# The best discount the current viewer would get right now, if any
query ApplicableDiscount($scope: String!, $subscriptionTierId: ID, $conversationId: ID) {
applicableDiscount(scope: $scope, subscriptionTierId: $subscriptionTierId, conversationId: $conversationId) {
discountType discountValue discountAmount originalPrice discountedPrice
}
}
Subscription-gated conversations
A conversation can require a coin payment to join — independent of the creator's profile subscription. See the Messages doc for the full API.
# Create a subscription-gated group conversation
input ConversationCreateInput {
type: String! # "subscription"
participantIds: [ID!]!
name: String
}
# The Conversation model exposes:
# isSubscriptionRequired: Boolean!
# subscriptionPriceCoins: Int
# subscriberCount: Int!
# totalEarnings: Int!
Exclusive content
Posts with visibility: subscribers are only visible to the creator's active subscribers. The backend verifies the active subscription before returning the content. Users who are not subscribers see a blurred/locked preview and a subscribe prompt.