Skip to main content

GraphQL & Apollo

The Closegram API is 100% GraphQL. The backend uses Apollo Server with Express. The frontend uses Apollo Client with support for HTTP, WebSocket, and file uploads.

Endpoints

The backend exposes two separate GraphQL endpoints, each backed by its own ApolloServer instance and its own schema (apps/backend/api/server.js):

EndpointConsumerSchema
POST /web/graphqlfrontend-nextjs, iOS appClient schema — every root Query/Mutation field except the admin*-prefixed ones
POST /admin/graphqlfrontend-adminAdmin schema — only the admin*-prefixed root Query/Mutation fields (e.g. adminLogin, adminGetUsers, adminGetReports)

The legacy POST /graphql path was removed. The client (web + iOS) now uses /web/graphql, mirroring the admin panel's /admin/graphql. Set NEXT_PUBLIC_GRAPHQL_URL / NEXT_PUBLIC_WS_URL to /web/graphql for the web app; iOS uses /web/graphql in APIConfig.swift.

Both schemas are derived at startup from one combined schema via mapSchema/MapperKind, filtering root fields by whether their name matches /^admin[A-Z]/. This is a real schema split, not just routing: admin* operations are structurally absent from /web/graphql (they don't show up in introspection there), and regular client operations are absent from /admin/graphql.

WebSocket subscriptions (graphql-ws) are mounted on /web/graphql — there are no admin-panel subscriptions.

Both endpoints share the same context builder (graphql/context/index.js), which resolves context.user (regular Firebase-authenticated user) and context.admin (AdminUser JWT, signed with a separate secret) independently on every request — a request only ever populates the one that matches the Bearer token it received.

Backend resolvers

ResolverDescription
user.resolver.jsRegistration, login, follow, privacy, sessions
post.resolver.jsPost CRUD, feed, view tracking
post-interaction.resolver.jsReactions (6 types)
post-comment.resolver.jsNested comments
message.resolver.jsRich messages, reactions, polls, location
conversation.resolver.jsConversations and participants
call.resolver.jsVoice/video calls, viewer mode
notification.resolver.jsNotifications and settings
coin-package.resolver.jsCoin packages (admin)
coin-transaction.resolver.jsBalance and history
coin-tip.resolver.jsTips on content
coin-purchase.resolver.jsCoin package purchases
payment-transaction.resolver.jsStripe payment history
payment-methods.resolvers.jsSaved payment methods
subscription-tier.resolver.jsCreator subscription tiers
user-subscription.resolver.jsActive subscriptions
message-purchase.resolver.jsPaid messages (lock/unlock)
saved-post.resolver.jsSaved posts
saved-collection.resolver.jsPost collections
hashtag.resolver.jsHashtags and trending
content-report.resolver.jsContent reports
content-moderation.resolver.jsModeration with Google Cloud AI
verification.resolver.jsVerification badges
conversation-subscription.resolver.jsConversation GraphQL subscriptions
admin-user.resolver.jsUser management (admin)
admin-dashboard.resolver.jsAdmin dashboard
user-moderation.resolver.jsUser restrictions and moderation

Schema types

The schema is split into domain files under graphql/types/ and combined in graphql/typeDefs.js.

Custom scalars: DateTime, JSON, Upload.

GraphQL packages & code generation

The schema and every operation live in one source-of-truth package, and three codegen packages generate typed clients from it. Web and admin never share an operation.

packages/
graphql/ # @closegram/graphql — SINGLE SOURCE OF TRUTH
schema/
schema.web.graphqls # client (web + iOS) schema, downloaded from /web/graphql
schema.admin.graphqls # admin schema, downloaded from /admin/graphql
operations/
Web/** # client operations (frontend-nextjs + iOS)
Admin/** # admin operations (frontend-admin only)
apollo-web/ # @closegram/apollo-web — TS types + hooks for frontend-nextjs
apollo-admin/ # @closegram/apollo-admin — TS types + hooks for frontend-admin
apollo-swift/ # Swift package (product/module: ClosegramGraphQL) for apps/ios
  • apollo-web reads schema.web.graphqls + operations/Web/** → the TS client frontend-nextjs imports from @closegram/apollo-web.
  • apollo-admin reads both schemas (merged) + operations/Admin/** → the TS client frontend-admin imports from @closegram/apollo-admin.
  • apollo-swift reads schema.web.graphqls + operations/Web/** → the ClosegramGraphQL Swift module the iOS app imports. Its SwiftPM package is named apollo-swift (not apollo-ios) to avoid a name clash with Apollo's own apollo-ios dependency; the executable codegen target is codegen.

Regenerate everything from the repo root:

npm run codegen
# = codegen:schema (download BOTH schemas from the running backend via introspection)
# -> codegen:web -> codegen:admin -> codegen:ios

codegen:schema introspects the running backend, so it must be up (it hits /web/graphql and /admin/graphql). To regenerate types only from the committed schema snapshots, run codegen:web / codegen:admin / codegen:ios individually. iOS codegen is cd packages/apollo-swift && swift run codegen.

Rule: admin operations live only under operations/Admin and web operations only under operations/Web — the two frontends never share a query. Any admin-only field is admin*-prefixed in the backend so it structurally lands on the admin schema/endpoint.

Apollo Client (frontend)

Configured in 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
const authLink = setContext(async (_, { headers }) => {
const token = await getFirebaseToken(); // auto-refreshes before expiry
return { headers: { ...headers, authorization: token ? `Bearer ${token}` : "" } };
});

Cache with pagination

new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: relayStylePagination(),
myTransactions: relayStylePagination(),
myNotifications: relayStylePagination(),
conversationMessages: { keyArgs: ["conversationId"] },
},
},
},
})

Server context

Each request's context (graphql/context/index.js) resolves:

  • The authenticated user from the Bearer token (Firebase Admin SDK)
  • The authenticated admin (for admin-only resolvers) — see Endpoints above for how /admin/graphql is separated from /web/graphql
  • The WebSocket context for subscriptions (ws-context.js)

Active subscriptions

EventDescription
messageAddedNew message in conversation
messageUpdatedMessage edited
messageDeletedMessage deleted
messageReactionAddedNew reaction on a message
typingIndicatorTyping status change
messageReadRead receipt
conversationAddedNew conversation
conversationUpdatedConversation updated
callIncomingIncoming call
callStatusChangedCall status change
callEndedCall ended
participantJoined / participantLeftCall participant change
speakerRequestReceivedSpeaker request in group call
speakerRequestResponseSpeaker request response