Skip to main content

Account Moderators — Technical Reference

← Back to Account Moderators

Where this lives

Backend

Frontend

Technical implementation checklist

  • account_moderator table + model + associations (owner, moderator) — 9 permission flags: canPost, canClip, canLive, canStory, canCreateChats, canSendMessages, canComment, canReact, canManageComments
  • account_moderator_action table + model — audit trail of every action performed on an owner's behalf
  • addModerator / updateModeratorPermissionsrequire owner.twoFactorEnabled (throw moderators.requires_2fa otherwise) and a fresh step-up auth window (throw moderators.requires_reauth / REQUIRES_REAUTH otherwise)
  • verifyPasswordForModerators(password) — re-verifies the owner's password and opens a fresh 15-minute step-up window
  • removeModerator — no 2FA or step-up required
  • myModerators / accountsIModerate / moderatorActivity queries
  • resolveActingUser(callerId, actAsUserId, action) enforcement on createPost (post/clip), createStory, likePost, createLiveStream, createConversation, sendMessage, createComment, deleteComment
  • logAction writes to the audit trail whenever the effective user differs from the caller (best-effort, never throws)
  • Message.viaModerator — owner-only attribution of who really sent a message sent "as" them
  • "Publicar como" in the post composer, "Comentar como" in the comment box
  • canStory enforcement — createStory accepts actAsUserId and calls resolveActingUser(..., 'story', ...); this doc previously said canStory was defined end-to-end but never enforced. Corrected.
  • canReact / canManageComments enforcement — likePost (post-interaction.resolver.js) calls resolveActingUser(..., 'react', ...) and deleteComment (post-comment.resolver.js) calls resolveActingUser(..., 'manage_comment', ...); this doc previously said neither permission was ever checked by any resolver. Corrected.
  • Acting-as UI trigger on the live / new-chat / message composers — GoLiveModal.tsx (filtered by canLive), ConversationList.tsx (filtered by canCreateChats), and ChatView.tsx (filtered by canSendMessages) all now expose an "acting as" selector wired to actAsUserId, the same pattern CreatePostModal/PostModal already used; this doc previously said only posts/clips/comments were wired in the UI. Corrected.

GraphQL

type ModeratorPermissions {
canPost: Boolean! canClip: Boolean! canLive: Boolean! canCreateChats: Boolean! canSendMessages: Boolean!
canComment: Boolean! canManageComments: Boolean! canStory: Boolean! canReact: Boolean!
}

type AccountModerator {
id: ID!
owner: User
moderator: User
canPost: Boolean! canClip: Boolean! canLive: Boolean! canCreateChats: Boolean! canSendMessages: Boolean!
canComment: Boolean! canManageComments: Boolean! canStory: Boolean! canReact: Boolean!
createdAt: DateTime! updatedAt: DateTime!
}

input ModeratorPermissionsInput {
canPost: Boolean canClip: Boolean canLive: Boolean canCreateChats: Boolean canSendMessages: Boolean
canComment: Boolean canManageComments: Boolean canStory: Boolean canReact: Boolean
}

"A single entry in the account-moderator audit trail (who did what, when)."
type ModeratorAction {
id: ID!
moderator: User
action: String! # post | clip | live | story | create_chat | send_message | comment | manage_comment | react
actedAs: String! # owner | self
targetType: String # post | live_stream | conversation | message | comment
targetId: ID
metadata: JSON
createdAt: DateTime!
}

type Query {
myModerators: [AccountModerator!]!
accountsIModerate: [AccountModerator!]!
moderatorActivity(limit: Int, offset: Int, action: String, moderatorUserId: ID): [ModeratorAction!]!
}

type Mutation {
addModerator(userId: ID!, permissions: ModeratorPermissionsInput!): AccountModerator! # 2FA + step-up required
updateModeratorPermissions(userId: ID!, permissions: ModeratorPermissionsInput!): AccountModerator! # 2FA + step-up required
removeModerator(userId: ID!): Boolean!
verifyPasswordForModerators(password: String!): Boolean! # step-up re-auth
}

# The create/send inputs gain: actAsUserId: ID
# PostCreateInput / StoryCreateInput / LiveStreamCreateInput / ConversationCreateInput / MessageCreateInput / PostCommentCreateInput
# likePost(postId, interactionType, actAsUserId) and deleteComment(commentId, actAsUserId) take actAsUserId
# as a plain argument (not via an input type)

Permission → action map

PermissionActionResolver
canPostregular postcreatePost
canClipsingle-video clipcreatePost (clip detected)
canLivestart a livecreateLiveStream
canCreateChatscreate a conversationcreateConversation
canSendMessagessend a messagesendMessage
canCommentcomment / reply on a postcreateComment (post-comment.resolver.js)
canStorypost a storycreateStory
canReactreact/likelikePost (post-interaction.resolver.js)
canManageCommentsmoderate/delete a commentdeleteComment (post-comment.resolver.js)

Configuration

Run 20260721130000-create-account-moderator and 20260721150000-expand-account-moderator, then restart the backend. No new environment variables. Owner must enable 2FA (Settings → Security) before granting moderator access; adding/updating moderators also requires a fresh step-up window (services/step-up-auth.service.js, Redis-backed, 15 minutes), which the owner can (re)open via verifyPasswordForModerators.