Suscripciones de creador — Referencia técnica
← Volver a Suscripciones de creador
Dónde vive esto
Backend
apps/backend/graphql/resolvers/subscription-tier.resolver.js- operaciones CRUD y activación de niveles (createTier,activeCreatorTiers,myTiers, ...)apps/backend/graphql/resolvers/user-subscription.resolver.js- suscribirse/cancelar/renovar (subscribe,mySubscriptions,isSubscribedTo,subscriberRetention, ...)apps/backend/graphql/resolvers/subscription-offer.resolver.js- enlaces de oferta de prueba gratuita/descuento (createSubscriptionOffer,offerByToken,redeemSubscriptionTrial,applicableDiscount, ...)apps/backend/graphql/types/subscription-tier.type.js- esquema deSubscriptionTierapps/backend/graphql/types/user-subscription.type.js- esquema deUserSubscription,SubscriberRetentionPointapps/backend/graphql/types/subscription-offer.type.js- esquemas deSubscriptionOffer,OfferPreview,ApplicableDiscountapps/backend/managers/payment-managers/subscription-tier.manager.js- lógica de negocio de los niveles, llama asubscription-tier.access-service.jsapps/backend/managers/payment-managers/user-subscription.manager.js- lógica de suscribir/cancelar/renovar, deducción de monedas, y las estadísticas degetSubscriberRetention, llama auser-subscription.access-service.jsapps/backend/managers/payment-managers/subscription-offer.manager.js- CRUD de ofertas de prueba/descuento, elegibilidad (audience: new|returning|all), canje, y cálculo de descuentos tanto para el alcanceprofilecomogroupapps/backend/data-access-services/payment/subscription-tier.access-service.js- acceso a la base de datos de los nivelesapps/backend/data-access-services/payment/user-subscription.access-service.js- acceso a la base de datos de los registros de suscripciónapps/backend/data-access-services/subscription-offer/subscription-offer.access-service.js- acceso a la base de datos de las ofertasapps/backend/data-access-services/subscription-offer/subscription-offer-redemption.access-service.js- registros de canje por usuario (uno por oferta/usuario, usado para las verificaciones de elegibilidad)
Frontend
apps/frontend-nextjs/src/components/CreatePostModal.tsx- el selector de visibilidad de publicaciones incluye una opciónSubscribers(PostVisibility.Subscribers) que restringe una publicación a la suscripción del creadorapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx- obtieneactiveCreatorTiers/isSubscribedToy renderiza el llamado a la acción "Suscribirse" + el selector de niveles en el perfil público de un creador, invocandosubscribeapps/frontend-nextjs/src/page-components/settings/SubscriptionsSettingsPage.tsx-Configuración → Suscripciones: gestión de niveles del creador (createTier/updateTier/activateTier/deactivateTier/deleteTier) y la lista de suscripciones propias del suscriptor (mySubscriptions,cancelSubscription,renewSubscription)apps/frontend-nextjs/src/page-components/settings/SubscriptionOffersPage.tsx-Configuración → Ofertas de suscripción: interfaz del creador paramyOffers/createSubscriptionOffer/deactivateSubscriptionOffer, cubriendo tanto niveles de perfil como grupos de pagoapps/frontend-nextjs/src/page-components/OfferRedemptionPage.tsx- página pública de destino/offer/[token](offerByToken,redeemSubscriptionTrial), no requiere autenticación para previsualizarapps/frontend-nextjs/src/hooks/useApplicableDiscount.tsyapps/frontend-nextjs/src/components/subscriptions/DiscountedPrice.tsx- muestran un precio con descuento en los llamados a la acción de suscripción de niveles/grupos cuandoapplicableDiscountdevuelve algunoapps/frontend-nextjs/src/page-components/settings/InsightsAndToolsPage.tsx- pantalla de analíticas del creador, incluye un gráfico desubscriberRetentionjunto asubscriberCountapps/frontend-nextjs/src/components/chat/hooks/useConversationSubscriptionAccess.ts- acceso al grupo de pago del lado del espectador (hasConversationAccess,subscribeToConversation,cancelConversationSubscription), usado porChatView.tsx/SubscriptionPaywall.tsxy por la lista de grupos de pago enPublicProfilePage.tsx
Checklist de implementación técnica
-
createTier/updateTier/activateTier/deactivateTier/deleteTier— resolvers conectados ensubscription-tier.resolver.js; la gestión de niveles del frontend ahora vive enConfiguración → Suscripciones(SubscriptionsSettingsPage.tsx) -
activeCreatorTiers/subscribe— resolvers conectados ensubscription-tier.resolver.js/user-subscription.resolver.js; el frontend ahora renderiza un selector de niveles y el llamado a la acción "Suscribirse" enPublicProfilePage.tsx, además de la opción de visibilidad de publicaciónSubscribersenCreatePostModal.tsx -
mySubscriptions/cancelSubscription/renewSubscription/isSubscribedTo/subscriberCount— resolvers conectados enuser-subscription.resolver.js; el frontend está conectado a través deSubscriptionsSettingsPage.tsx(listar/cancelar/renovar) yPublicProfilePage.tsx(isSubscribedTo) -
mySubscribers— resolver conectado enuser-subscription.resolver.js; no se encontró ninguna pantalla en el frontend para la lista de suscriptores del lado del creador -
subscriberRetention— resolver conectado enuser-subscription.resolver.js, calculado poruserSubscriptionManager.getSubscriberRetention(conteos de nuevos/perdidos/activos agrupados por mes); gráfico en el frontend enInsightsAndToolsPage.tsx -
subscribeToConversation/hasConversationAccess/cancelConversationSubscription— resolvers conectados enconversation-subscription.resolver.js; el frontend está conectado a través deuseConversationSubscriptionAccess.ts→ChatView.tsx/SubscriptionPaywall.tsxy la lista de grupos de pago enPublicProfilePage.tsx -
enableConversationSubscription/disableConversationSubscription— resolvers conectados; botones en el frontend enConversationDetailsPanel.tsx(handleEnableSubscription/handleDisableSubscription) permiten al creador de un chat grupal activar/desactivar el acceso de pago -
conversationSubscriptionStats/mySubscriptionEarnings/conversationSubscribers/grantFreeConversationAccess— resolvers conectados enconversation-subscription.resolver.js; no se encontró uso en el frontend de ninguno de estos campos - Restricción de publicaciones con
visibility: subscribers— el valor del enum se puede seleccionar enCreatePostModal.tsx, ypost.manager.js(apps/backend/managers/post-managers/post.manager.js, líneas ~572-577) verificavisibility === 'subscribers'medianteuserSubscriptionManager.isSubscribed(...), lanzando un error si el espectador no está suscrito — aplicado del lado del servidor - Ofertas de suscripción (
SubscriptionOffer) — enlaces de prueba gratuita y descuento para un nivel de perfil o un grupo de pago. El CRUD del creador (createSubscriptionOffer/updateSubscriptionOffer/deactivateSubscriptionOffer/deleteSubscriptionOffer) está conectado enSubscriptionOffersPage.tsx(Configuración → Ofertas de suscripción); el canje público (offerByToken/redeemSubscriptionTrial) está conectado enOfferRedemptionPage.tsxen/offer/[token];applicableDiscountestá conectado a través deuseApplicableDiscount.ts/DiscountedPrice.tsxpara mostrar precios con descuento en los llamados a la acción de suscripción
Niveles de suscripción (SubscriptionTier)
| Campo | Descripción |
|---|---|
name | Nombre del nivel (p. ej. "Fan", "VIP") |
description | Descripción de los beneficios |
coinPrice | Precio en monedas por período |
benefits | Lista de cadenas de beneficios |
isActive | Indica si el nivel está abierto a nuevos suscriptores |
subscriberCount | Suscriptores activos actuales |
activeCreatorTiers devuelve los niveles que un creador ha publicado y puesto a disposición. Úsalo para renderizar la página de "Suscribirse" en el perfil de un creador. myTiers devuelve los propios niveles del creador autenticado — usado en la pantalla de configuración del creador.
createTier agrega un nuevo nivel. El nivel empieza inactivo; llama a activateTier para abrirlo a suscriptores. updateTier edita el nombre, la descripción, el precio o los beneficios de un nivel. deactivateTier deja de aceptar nuevas suscripciones pero no cancela las existentes. deleteTier elimina permanentemente el nivel — solo es posible si no tiene suscriptores activos.
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) }
Suscripciones de usuario (UserSubscription)
mySubscriptions devuelve todas las suscripciones a creadores del usuario actual — tanto activas como canceladas. currentPeriodStart / currentPeriodEnd indican al cliente cuándo comenzó y termina el ciclo de facturación actual.
mySubscribers es la vista del lado del creador: quién está suscrito a ti y en qué nivel.
isSubscribedTo es una verificación booleana ligera — úsala para restringir contenido exclusivo sin obtener el objeto de suscripción completo. subscriberCount devuelve un único entero para la fila de estadísticas del perfil del creador.
subscribe crea una nueva suscripción pagando el coinPrice del nivel. Las monedas se deducen de inmediato. cancelSubscription marca la suscripción como cancelled — el acceso continúa hasta currentPeriodEnd. renewSubscription renueva manualmente una suscripción cancelada o expirada.
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 }
}
}
# Booleano ligero para restringir contenido exclusivo
query IsSubscribedTo($creatorId: ID!) { isSubscribedTo(creatorId: $creatorId) }
query SubscriberCount($creatorId: ID!) { subscriberCount(creatorId: $creatorId) }
# Iniciar una suscripción (deduce coinPrice de la billetera de inmediato)
mutation Subscribe($input: UserSubscriptionCreateInput!) { subscribe(input: $input) { id status } }
# Cancelar — el acceso continúa hasta el final del período actual
mutation CancelSubscription($subscriptionId: ID!) { cancelSubscription(subscriptionId: $subscriptionId) { status } }
# Renovar una suscripción cancelada o expirada
mutation RenewSubscription($subscriptionId: ID!) { renewSubscription(subscriptionId: $subscriptionId) { status } }
subscriberRetention devuelve un SubscriberRetentionPoint por mes calendario (6 por defecto, máximo 24) con los conteos de suscriptores nuevos/perdidos/activos al final, y las tasas de pérdida/retención, para las suscripciones del perfil del creador autenticado:
query SubscriberRetention($months: Int) {
subscriberRetention(months: $months) {
month newSubscribers churned activeAtEnd churnRate retentionRate
}
}
Ofertas de suscripción (SubscriptionOffer)
Un creador puede generar un enlace compartible que otorga ya sea una prueba gratuita (kind: trial) o un descuento en el primer pago (kind: discount) para un nivel de perfil (scope: profile) o una conversación grupal de pago (scope: group). Las ofertas están limitadas por maxRedemptions, pueden expirar (expiresAt), y apuntan a una audience: new (nunca se ha suscrito), returning (se suscribió antes, ahora inactivo), o all. El canje se rastrea por usuario para que la misma oferta no pueda canjearse dos veces.
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
}
}
# Vista previa pública — no requiere autenticación — renderizada en /offer/<token>
query OfferByToken($token: String!) {
offerByToken(token: $token) {
kind scope name trialDays discountType discountValue
coinPrice isActive expired capReached redemptionsLeft
}
}
# Canjear un enlace de prueba gratuita (otorga acceso que expira al final de la prueba, sin cobro automático)
mutation RedeemSubscriptionTrial($token: String!) {
redeemSubscriptionTrial(token: $token) { success scope trialEndsAt }
}
# El mejor descuento que el espectador actual obtendría en este momento, si existe alguno
query ApplicableDiscount($scope: String!, $subscriptionTierId: ID, $conversationId: ID) {
applicableDiscount(scope: $scope, subscriptionTierId: $subscriptionTierId, conversationId: $conversationId) {
discountType discountValue discountAmount originalPrice discountedPrice
}
}
Conversaciones restringidas por suscripción
Una conversación puede requerir un pago en monedas para unirse — independiente de la suscripción al perfil del creador. Consulta el documento de Mensajes para la API completa.
# 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!
Contenido exclusivo
Las publicaciones con visibility: subscribers solo son visibles para los suscriptores activos del creador. El backend verifica la suscripción activa antes de devolver el contenido. Los usuarios que no son suscriptores ven una vista previa borrosa/bloqueada y una invitación a suscribirse.