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):
| Endpoint | Consumer | Schema |
|---|---|---|
POST /web/graphql | frontend-nextjs, iOS app | Client schema — every root Query/Mutation field except the admin*-prefixed ones |
POST /admin/graphql | frontend-admin | Admin schema — only the admin*-prefixed root Query/Mutation fields (e.g. adminLogin, adminGetUsers, adminGetReports) |
The legacy
POST /graphqlpath was removed. The client (web + iOS) now uses/web/graphql, mirroring the admin panel's/admin/graphql. SetNEXT_PUBLIC_GRAPHQL_URL/NEXT_PUBLIC_WS_URLto/web/graphqlfor the web app; iOS uses/web/graphqlinAPIConfig.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
| Resolver | Description |
|---|---|
user.resolver.js | Registration, login, follow, privacy, sessions |
post.resolver.js | Post CRUD, feed, view tracking |
post-interaction.resolver.js | Reactions (6 types) |
post-comment.resolver.js | Nested comments |
message.resolver.js | Rich messages, reactions, polls, location |
conversation.resolver.js | Conversations and participants |
call.resolver.js | Voice/video calls, viewer mode |
notification.resolver.js | Notifications and settings |
coin-package.resolver.js | Coin packages (admin) |
coin-transaction.resolver.js | Balance and history |
coin-tip.resolver.js | Tips on content |
coin-purchase.resolver.js | Coin package purchases |
payment-transaction.resolver.js | Stripe payment history |
payment-methods.resolvers.js | Saved payment methods |
subscription-tier.resolver.js | Creator subscription tiers |
user-subscription.resolver.js | Active subscriptions |
message-purchase.resolver.js | Paid messages (lock/unlock) |
saved-post.resolver.js | Saved posts |
saved-collection.resolver.js | Post collections |
hashtag.resolver.js | Hashtags and trending |
content-report.resolver.js | Content reports |
content-moderation.resolver.js | Moderation with Google Cloud AI |
verification.resolver.js | Verification badges |
conversation-subscription.resolver.js | Conversation GraphQL subscriptions |
admin-user.resolver.js | User management (admin) |
admin-dashboard.resolver.js | Admin dashboard |
user-moderation.resolver.js | User 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 clientfrontend-nextjsimports from@closegram/apollo-web. - apollo-admin reads both schemas (merged) +
operations/Admin/**→ the TS clientfrontend-adminimports from@closegram/apollo-admin. - apollo-swift reads
schema.web.graphqls+operations/Web/**→ theClosegramGraphQLSwift module the iOS app imports. Its SwiftPM package is namedapollo-swift(notapollo-ios) to avoid a name clash with Apollo's ownapollo-iosdependency; the executable codegen target iscodegen.
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
Auth link
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/graphqlis separated from/web/graphql - The WebSocket context for subscriptions (
ws-context.js)
Active subscriptions
| Event | Description |
|---|---|
messageAdded | New message in conversation |
messageUpdated | Message edited |
messageDeleted | Message deleted |
messageReactionAdded | New reaction on a message |
typingIndicator | Typing status change |
messageRead | Read receipt |
conversationAdded | New conversation |
conversationUpdated | Conversation updated |
callIncoming | Incoming call |
callStatusChanged | Call status change |
callEnded | Call ended |
participantJoined / participantLeft | Call participant change |
speakerRequestReceived | Speaker request in group call |
speakerRequestResponse | Speaker request response |