Post Collaborators — Technical Reference
Where this lives
Backend
apps/backend/managers/post-managers/post-collaborator.manager.js— invite/accept/reject/remove collaborator and collaboration-stats business logicapps/backend/data-access-services/post/post-collaborator.access-service.js—PostCollaboratordatabase queriesapps/backend/graphql/types/post-collaborator.type.js/apps/backend/graphql/resolvers/post-collaborator.resolver.js— the GraphQL schema and resolvers wiring the manager above into the live API (auto-loaded into the schema the same way every othergraphql/types/graphql/resolversfile is, vialoadFilesSyncingraphql/typeDefs.js/graphql/resolvers.js)apps/backend/managers/post-managers/post.manager.js—getUserPosts/getFeedpull in posts the caller/their followees co-author as an accepted collaborator (viapostCollaboratorAccessService.getAcceptedPostIdsForUsers), so a collaborative post surfaces on the collaborator's profile grid and their followers' feeds
The API described below is reachable — inviteCollaborator, bulkInviteCollaborators, acceptCollaboration, rejectCollaboration, removeCollaborator, postCollaborators, myPendingCollaborations, collaborationStats and isCollaborator are all live GraphQL fields.
Frontend
apps/frontend-nextjs/src/page-components/settings/CollaborationInvitesPage.tsx(mounted at/settings/collaborations) — lists the caller's pending invites (myPendingCollaborations), Accept/Decline buttons (acceptCollaboration/rejectCollaboration), and a 3-stat header (collaborationStats)apps/frontend-nextjs/src/components/PostOptionsMenu.tsxhas an owner-only "Manage collaborators" entry that opensCollaboratorsModalwithisOwnerset. Fixed this session: the modal used to no longer talk to the post-collaborator API at all — it queriedpost(id).taggedUsers(the separate tap-to-tag feature backed byPostTag/PostMention) and just listed those people, read-only. It's now rewired to the realpostCollaboratorsquery (management mode, triggered when apostIdis passed without ausersprop), showing each collaborator with a pending/accepted/declined status pill. WhenisOwneris set it also renders an invite form (username search reusing the tag-peoplesearchUsersquery, callinginviteCollaboratorfor a single pick orbulkInviteCollaboratorswhen multiple are selected) and a "Remove" action per row (removeCollaborator) — both owner-gated, since onlyPostOptionsMenu's owner-only entry ever passesisOwner.PostCard.tsx's read-only "With @user and N more" line still passesusersdirectly (notpostId), so that plain display path is unaffected and still doesn't querypostCollaborators.
Technical implementation checklist
-
inviteCollaboratormutation — implemented (post-collaborator.manager.js), wired to the schema (post-collaborator.resolver.js), and now called fromCollaboratorsModal's invite form (owner-only). Fixed this session — previously reachable over GraphQL only, no frontend caller -
acceptCollaboration/rejectCollaborationmutations — implemented, wired, and used byCollaborationInvitesPage.tsx -
removeCollaboratormutation — implemented, wired, and now called fromCollaboratorsModal's "Remove" row action (owner-only). Fixed this session — previously no frontend caller -
bulkInviteCollaboratorsmutation — implemented, wired, and now called fromCollaboratorsModal's invite form when more than one user is selected. Fixed this session — previously no frontend caller -
postCollaborators/myPendingCollaborationsqueries — both implemented and wired;postCollaboratorsis now called byCollaboratorsModalin management mode (fixed this session — it previously queriedpost(id).taggedUsersinstead), andmyPendingCollaborationscontinues to be used byCollaborationInvitesPage.tsx -
collaborationStatsquery — implemented, wired, and used byCollaborationInvitesPage.tsx(this is the caller's own stats across all of their collaborations, not scoped to one post) -
isCollaboratorquery — implemented and wired; still no frontend caller - Collaborative post appears on all collaborators' profiles and feeds —
post.manager.js'sgetUserPosts/getFeedpull in accepted-collaborator posts viagetAcceptedPostIdsForUsers
How it works
The original post creator invites one or more collaborators via inviteCollaborator/bulkInviteCollaborators, from CollaboratorsModal's invite form (owner-only, opened via PostOptionsMenu's "Manage collaborators" entry). Each invited user can accept or reject the invitation from Settings → Collaboration invites. Once accepted, the post appears on the collaborator's own profile grid and in the feeds of their followers, just as if they had posted it themselves. Removing a collaborator (removeCollaborator, also owner-only from CollaboratorsModal) deletes the collaboration row outright rather than flipping it to a "removed" status. Fixed this session: inviting, bulk-inviting, removing, and browsing the roster now all have real frontend UI in CollaboratorsModal; previously only the accept/reject half of this flow (CollaborationInvitesPage.tsx) did, and the modal itself displayed unrelated tagged-user data instead of real collaboration records.
Data model
PostCollaborator (apps/backend/database/models/PostCollaborator.js, table post_collaborator):
| Field | Type | Description |
|---|---|---|
id | UUID | Collaborator record ID |
postId | UUID (column post_id) | The post being collaborated on |
userId | UUID (column user_id) | The invited/collaborating user's ID |
invitedBy | UUID (column invited_by) | User ID of the post owner who sent the invite |
status | String(50) | pending, accepted, or rejected (defaults to pending; there is no removed status — removal deletes the row) |
createdAt / updatedAt | DateTime (columns created_at/updated_at) | Standard timestamps |
There is no sharedToFeed, invitedAt, or respondedAt column — feed/profile visibility is derived at read time from status === 'accepted', and createdAt/updatedAt cover invite/response timing.
GraphQL API
Invite a collaborator
inviteCollaborator sends an invitation to a specific user. The caller must be the post's owner (enforced in the manager); inviting yourself, a nonexistent post, or a user who's already invited/collaborating all raise an error. The invite enters pending status and creates a collaboration_invite notification for the invitee.
mutation InviteCollaborator($postId: ID!, $userId: ID!) {
inviteCollaborator(postId: $postId, userId: $userId) {
id
status
createdAt
user { id username profilePicture }
}
}
Bulk invite
bulkInviteCollaborators calls inviteCollaborator once per userId; a failure for one user (already invited, self-invite, etc.) is collected in errors rather than aborting the whole batch, and success is false if any invite failed.
mutation BulkInviteCollaborators($postId: ID!, $userIds: [ID!]!) {
bulkInviteCollaborators(postId: $postId, userIds: $userIds) {
success
invited
failed
errors { userId error }
}
}
Respond to an invite
acceptCollaboration/rejectCollaboration let the invitee (only) respond to their own pending invite. Accepting sets status: accepted and notifies the inviter (collaboration_accepted); rejecting sets status: rejected and notifies the inviter (collaboration_rejected). Either call fails if the invite isn't pending or the caller isn't the invitee.
mutation AcceptCollaboration($collaborationId: ID!) {
acceptCollaboration(collaborationId: $collaborationId) { id status }
}
mutation RejectCollaboration($collaborationId: ID!) {
rejectCollaboration(collaborationId: $collaborationId) { id status }
}
Remove a collaborator
removeCollaborator deletes the collaboration row outright. Only the user who sent the invite (invitedBy, i.e. the post owner) can do this; it notifies the removed collaborator (collaboration_removed).
mutation RemoveCollaborator($collaborationId: ID!) {
removeCollaborator(collaborationId: $collaborationId)
}
Query collaborators / invites
postCollaborators returns a post's collaboration rows (any status by default, or filtered via status) — there's no ownership check in the resolver, so any authenticated caller can read it. myPendingCollaborations returns the caller's own pending invitations. isCollaborator reports whether the caller is an accepted collaborator on a post.
query PostCollaborators($postId: ID!) {
postCollaborators(postId: $postId, status: "accepted", limit: 20, offset: 0) {
id status createdAt
user { id username profilePicture }
}
}
query MyPendingCollaborations {
myPendingCollaborations {
id status createdAt
inviter { id username profilePicture }
post { id text }
}
}
query IsCollaborator($postId: ID!) {
isCollaborator(postId: $postId)
}
Collaboration stats
collaborationStats returns the caller's own collaboration counts across every post they've been invited to — not scoped to a single post.
query CollaborationStats {
collaborationStats {
totalCollaborations
accepted
pending
rejected
}
}
Feed behavior
When a user accepts a collaboration invite, post.manager.js's getUserPosts includes the post on their own profile grid, and getFeed includes it in the feeds of anyone who follows them — both derived per-request from postCollaboratorAccessService.getAcceptedPostIdsForUsers. Both lookups are best-effort: a failure there is logged and swallowed rather than breaking the profile/feed load.