GraphQL y Apollo
La API de Closegram es 100% GraphQL. El backend usa Apollo Server con Express. El frontend usa Apollo Client con soporte para HTTP, WebSocket y carga de archivos.
Endpoints
El backend expone dos endpoints de GraphQL separados, cada uno respaldado por su propia instancia de ApolloServer y su propio esquema (apps/backend/api/server.js):
| Endpoint | Consumidor | Esquema |
|---|---|---|
POST /web/graphql | frontend-nextjs, app iOS | Esquema de cliente — todos los campos raíz Query/Mutation excepto los que tienen el prefijo admin* |
POST /admin/graphql | frontend-admin | Esquema de administración — solo los campos raíz Query/Mutation con el prefijo admin* (por ejemplo, adminLogin, adminGetUsers, adminGetReports) |
La ruta legacy
POST /graphqlfue eliminada. El cliente (web + iOS) ahora usa/web/graphql, espejando/admin/graphqldel panel. ConfiguraNEXT_PUBLIC_GRAPHQL_URL/NEXT_PUBLIC_WS_URLa/web/graphqlpara la web; iOS usa/web/graphqlenAPIConfig.swift.
Ambos esquemas se derivan al iniciar a partir de un esquema combinado mediante mapSchema/MapperKind, filtrando los campos raíz según si su nombre coincide con /^admin[A-Z]/. Esto es una división real de esquema, no solo de enrutamiento: las operaciones admin* están estructuralmente ausentes de /web/graphql (no aparecen en la introspección ahí), y las operaciones regulares de cliente están ausentes de /admin/graphql.
Las suscripciones por WebSocket (graphql-ws) están montadas en /web/graphql — no existen suscripciones para el panel de administración.
Ambos endpoints comparten el mismo constructor de contexto (graphql/context/index.js), que resuelve context.user (usuario regular autenticado con Firebase) y context.admin (JWT de AdminUser, firmado con un secreto separado) de forma independiente en cada solicitud — una solicitud solo llega a completar el que coincide con el token Bearer que recibió.
Resolvers del backend
| Resolver | Descripción |
|---|---|
user.resolver.js | Registro, inicio de sesión, seguir, privacidad, sesiones |
post.resolver.js | CRUD de publicaciones, feed, seguimiento de vistas |
post-interaction.resolver.js | Reacciones (6 tipos) |
post-comment.resolver.js | Comentarios anidados |
message.resolver.js | Mensajes enriquecidos, reacciones, encuestas, ubicación |
conversation.resolver.js | Conversaciones y participantes |
call.resolver.js | Llamadas de voz/video, modo espectador |
notification.resolver.js | Notificaciones y configuración |
coin-package.resolver.js | Paquetes de monedas (administración) |
coin-transaction.resolver.js | Saldo e historial |
coin-tip.resolver.js | Propinas en contenido |
coin-purchase.resolver.js | Compras de paquetes de monedas |
payment-transaction.resolver.js | Historial de pagos de Stripe |
payment-methods.resolvers.js | Métodos de pago guardados |
subscription-tier.resolver.js | Niveles de suscripción de creadores |
user-subscription.resolver.js | Suscripciones activas |
message-purchase.resolver.js | Mensajes pagos (bloqueo/desbloqueo) |
saved-post.resolver.js | Publicaciones guardadas |
saved-collection.resolver.js | Colecciones de publicaciones |
hashtag.resolver.js | Hashtags y tendencias |
content-report.resolver.js | Reportes de contenido |
content-moderation.resolver.js | Moderación con Google Cloud AI |
verification.resolver.js | Insignias de verificación |
conversation-subscription.resolver.js | Suscripciones de GraphQL de conversaciones |
admin-user.resolver.js | Gestión de usuarios (administración) |
admin-dashboard.resolver.js | Dashboard de administración |
user-moderation.resolver.js | Restricciones y moderación de usuarios |
Tipos del esquema
El esquema está dividido en archivos por dominio bajo graphql/types/ y combinado en graphql/typeDefs.js.
Escalares personalizados: DateTime, JSON, Upload.
Apollo Client (frontend)
Paquetes GraphQL y generación de código
El esquema y todas las operaciones viven en un único paquete fuente de verdad, y tres paquetes de codegen generan clientes tipados a partir de él. Web y admin nunca comparten una operación.
packages/
graphql/ # @closegram/graphql — ÚNICA FUENTE DE VERDAD
schema/
schema.web.graphqls # esquema cliente (web + iOS), descargado de /web/graphql
schema.admin.graphqls # esquema admin, descargado de /admin/graphql
operations/
Web/** # operaciones de cliente (frontend-nextjs + iOS)
Admin/** # operaciones de admin (solo frontend-admin)
apollo-web/ # @closegram/apollo-web — tipos TS + hooks para frontend-nextjs
apollo-admin/ # @closegram/apollo-admin — tipos TS + hooks para frontend-admin
apollo-swift/ # paquete Swift (producto/módulo: ClosegramGraphQL) para apps/ios
- apollo-web lee
schema.web.graphqls+operations/Web/**→ cliente TS quefrontend-nextjsimporta de@closegram/apollo-web. - apollo-admin lee ambos esquemas (fusionados) +
operations/Admin/**→ cliente TS quefrontend-adminimporta de@closegram/apollo-admin. - apollo-swift lee
schema.web.graphqls+operations/Web/**→ el módulo SwiftClosegramGraphQLque importa la app iOS. Su paquete SwiftPM se llamaapollo-swift(noapollo-ios) para evitar el choque de nombre con la dependenciaapollo-iosde Apollo; el target ejecutable de codegen escodegen.
Regenera todo desde la raíz del repo:
npm run codegen
# = codegen:schema (descarga AMBOS esquemas del backend en ejecución por introspección)
# -> codegen:web -> codegen:admin -> codegen:ios
codegen:schema hace introspección al backend en ejecución, así que debe estar arriba (pega a /web/graphql y /admin/graphql). Para regenerar solo tipos desde los snapshots del esquema, corre codegen:web / codegen:admin / codegen:ios por separado. El de iOS es cd packages/apollo-swift && swift run codegen.
Regla: las operaciones admin viven solo en operations/Admin y las de web solo en operations/Web — los dos frontends nunca comparten una query. Todo campo exclusivo de admin lleva prefijo admin* en el backend para caer estructuralmente en el esquema/endpoint de admin.
Apollo Client (frontend)
Configurado en packages/apollo-web (@closegram/apollo-web):
// Link chain
authLink // attaches Firebase token in Authorization header
httpLink // HTTP queries and mutations
wsLink // WebSocket subscriptions
uploadLink // multipart for file uploads
splitLink // routes subscriptions → wsLink, rest → httpLink/uploadLink
Auth link
const authLink = setContext(async (_, { headers }) => {
const token = await getFirebaseToken(); // auto-refreshes before expiry
return { headers: { ...headers, authorization: token ? `Bearer ${token}` : "" } };
});
Caché con paginación
new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: relayStylePagination(),
myTransactions: relayStylePagination(),
myNotifications: relayStylePagination(),
conversationMessages: { keyArgs: ["conversationId"] },
},
},
},
})
Contexto del servidor
El contexto de cada solicitud (graphql/context/index.js) resuelve:
- El usuario autenticado a partir del token Bearer (Firebase Admin SDK)
- El administrador autenticado (para resolvers exclusivos de administración) — ver Endpoints arriba para saber cómo se separa
/admin/graphqlde/web/graphql - El contexto de WebSocket para las suscripciones (
ws-context.js)
Suscripciones activas
| Evento | Descripción |
|---|---|
messageAdded | Mensaje nuevo en la conversación |
messageUpdated | Mensaje editado |
messageDeleted | Mensaje eliminado |
messageReactionAdded | Nueva reacción en un mensaje |
typingIndicator | Cambio en el estado de escritura |
messageRead | Confirmación de lectura |
conversationAdded | Conversación nueva |
conversationUpdated | Conversación actualizada |
callIncoming | Llamada entrante |
callStatusChanged | Cambio de estado de la llamada |
callEnded | Llamada finalizada |
participantJoined / participantLeft | Cambio de participante en la llamada |
speakerRequestReceived | Solicitud para hablar en una llamada grupal |
speakerRequestResponse | Respuesta a la solicitud para hablar |