Skip to main content

Creator Subscriptions — Technical Reference

← Back to Creator Subscriptions

Where this lives

Backend

Frontend

Technical implementation checklist

  • createTier / updateTier / activateTier / deactivateTier / deleteTier — resolvers wired in subscription-tier.resolver.js; frontend tier management now lives on Settings → Subscriptions (SubscriptionsSettingsPage.tsx)
  • activeCreatorTiers / subscribe — resolvers wired in subscription-tier.resolver.js/user-subscription.resolver.js; frontend now renders a tier picker and "Subscribe" call-to-action on PublicProfilePage.tsx, in addition to the Subscribers post-visibility option in CreatePostModal.tsx
  • mySubscriptions / cancelSubscription / renewSubscription / isSubscribedTo / subscriberCount — resolvers wired in user-subscription.resolver.js; frontend wired via SubscriptionsSettingsPage.tsx (list/cancel/renew) and PublicProfilePage.tsx (isSubscribedTo)
  • mySubscribers — resolver wired in user-subscription.resolver.js; no frontend screen found for the creator-side subscriber list
  • subscriberRetention — resolver wired in user-subscription.resolver.js, computed by userSubscriptionManager.getSubscriberRetention (month-bucketed new/churned/active counts); frontend chart on InsightsAndToolsPage.tsx
  • subscribeToConversation / hasConversationAccess / cancelConversationSubscription — resolvers wired in conversation-subscription.resolver.js; frontend wired via useConversationSubscriptionAccess.tsChatView.tsx/SubscriptionPaywall.tsx and the paid-groups list on PublicProfilePage.tsx
  • enableConversationSubscription / disableConversationSubscription — resolvers wired; frontend buttons in ConversationDetailsPanel.tsx (handleEnableSubscription/handleDisableSubscription) let a group-chat creator turn paid access on/off
  • conversationSubscriptionStats / mySubscriptionEarnings / conversationSubscribers / grantFreeConversationAccess — resolvers wired in conversation-subscription.resolver.js; no frontend usage found for any of these fields
  • visibility: subscribers post gating — the enum value is selectable in CreatePostModal.tsx, and post.manager.js (apps/backend/managers/post-managers/post.manager.js, lines ~572-577) checks visibility === 'subscribers' via userSubscriptionManager.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 in SubscriptionOffersPage.tsx (Settings → Subscription offers); public redemption (offerByToken/redeemSubscriptionTrial) wired in OfferRedemptionPage.tsx at /offer/[token]; applicableDiscount wired via useApplicableDiscount.ts/DiscountedPrice.tsx to show discounted prices on subscribe CTAs

Subscription tiers (SubscriptionTier)

FieldDescription
nameTier name (e.g. "Fan", "VIP")
descriptionBenefits description
coinPricePrice in coins per period
benefitsList of benefit strings
isActiveWhether the tier is open for new subscribers
subscriberCountCurrent 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.