Identity Verification Review
Admins review the age/identity (KYC-style) verification queue — government ID + selfie submissions required before a creator can request a cashout. This is a separate system from the notability/badge verification queue at /verification (see User Verification): different GraphQL operations, different permission (MODERATE_CONTENT, not VERIFY_USERS), and this page has a document/selfie review modal the badge queue doesn't.
This is the admin-side counterpart to Age & Identity Verification's "Admin review queue" checkbox (already marked done on that page) — it was previously undocumented on the admin side specifically. New doc.
Implementation checklist
- List pending identity-verification submissions (
adminPendingIdentityVerifications) - View submitted document + selfie photos in a review modal
- Approve a submission (
adminApproveIdentityVerification) - Reject a submission with a reason (
adminRejectIdentityVerification)
Where this lives
Backend
apps/backend/graphql/types/identity-verification.type.js— schema (its header comment frames this as the "identity (KYC-style) half" of Age & Identity Verification, distinct from the badge system; approval gates real-money cashouts)apps/backend/graphql/resolvers/identity-verification.resolver.js—adminPendingIdentityVerifications→identityVerificationManager.getPendingRequests;adminApproveIdentityVerification/adminRejectIdentityVerification→identityVerificationManager.approve/.reject. Note: these were renamed this cycle to beadmin-prefixed, so the schema split routes them onto the admin schema (/admin/graphql) — even though the resolver file itself still lives outside theadmin/folder. Each also enforcescontext.admin+ theMODERATE_CONTENTpermission inside the resolver.apps/backend/managers/user-managers/identity-verification.manager.js— approve/reject business logic (a separate file fromverification-badges.manager.js)
Frontend — apps/frontend-admin/src/app/moderation/identity-verifications/page.tsx. The list table shows username, email and identityVerificationRequestedAt; a review modal opened per-row renders identityDocumentUrl/identitySelfieUrl as clickable images plus a reject-reason textarea. Gated on MODERATE_CONTENT, matching the server-side check.
GraphQL reference
query PendingIdentityVerifications($limit: Int, $offset: Int) {
adminPendingIdentityVerifications(limit: $limit, offset: $offset) {
id username email firstName lastName profilePicture
identityVerificationStatus identityDocumentUrl
identitySelfieUrl identityVerificationRequestedAt
}
}
mutation ApproveIdentityVerification($userId: ID!) {
adminApproveIdentityVerification(userId: $userId) { identityVerificationStatus }
}
mutation RejectIdentityVerification($userId: ID!, $reason: String!) {
adminRejectIdentityVerification(userId: $userId, reason: $reason) { identityVerificationStatus }
}
Note: adminPendingIdentityVerifications returns a plain list ([IdentityVerificationRequest!]!), not a paginated { total, requests } wrapper — the frontend pages via limit/offset and disables "next" once a page comes back shorter than the page size. id on each item is the user's id (used directly as userId in the mutations); there is no separate user { id username } sub-object. Both mutations return an IdentityVerificationStatusResult (identityVerificationStatus, identityVerificationRequestedAt, identityVerifiedAt, identityRejectionReason), not a { success, message } payload, and reason on adminRejectIdentityVerification is required.