Build a social app
09 · Moderation

A queue, an action log, and an auto-hide threshold.

End-users file flags from your backend (PAK lane). Your team works the queue from the workspace admin lane. Every action is recorded in an append-only audit table.


1

File a flag

PAK lane. Idempotent on (reporter, target) while the flag is open — re-submitting returns the existing flag with created: false. target_kind is one of post, comment, actor, or direct_message (a DM flag requires the reporter to be in the conversation). reason is optional and comes back as an empty string when omitted.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "reporter_actor_id": "<bob>",
    "target_kind": "post",
    "target_id": "<post-uuid>",
    "reason": "Off-topic for the community"
  }' \
  https://social.productcraft.co/v1/communities/<c>/flags
response.json
{
  "flag": {
    "id": "9d584af5-980f-423b-9360-84b566b3b3d1",
    "community_id": "<c>",
    "target_kind": "post",
    "target_id": "<post-uuid>",
    "reporter_actor_id": "<bob>",
    "reason": "Off-topic for the community",
    "status": "open",
    "created_at": "2026-05-02T19:36:05.521Z",
    "updated_at": "2026-05-02T19:36:05.521Z"
  },
  "created": true,
  "auto_hidden": false
}

2

Auto-hide threshold

When the number of distinct reporters on the same target hits community.settings.auto_hide_flag_threshold (default 3), Social flips the target to status="hidden" and the response carries auto_hidden: true. Hidden content stops appearing in feeds and is invisible to non-author requesters until a moderator applies unhide.


3

Work the queue (admin lane)

The admin endpoints are workspace-scoped. The same Authorization: Bearer pcft_live_... key you use on the customer lane works here (policy permitting); a PlatformUser bearer or the platform session cookie work too, which is what console-adjacent internal tools use. Required permission: social.moderation.queue.read.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/workspaces/<ws>/communities/<c>/moderation/flags?status=open&limit=20"

# Filter by status: open | dismissed | actioned (anything else is a 400)
# Cursor-paginated, newest first.

4

Apply an action

One action resolves the flag — and resolves every other open flag on the same target in one transition, so the queue self-clears. Required permission: social.moderate.

  • hide / unhide — set / clear status="hidden" on the post or comment. A hidden post is still readable via GET /posts/:id (you'll see status: "hidden" in the response), it just stops appearing in feeds and non-author listings.
  • remove / restore — set / clear status="removed" (a stricter soft-delete than hidden). A removed post returns 404 from GET /posts/:id entirely. restore brings it back to published.
  • warn / ban — audit-only on this endpoint; they record the decision without touching the target. For real actor-level enforcement use PATCH /actors/:actorId with status="suspended", or the shadow-ban endpoints on the same admin controller.
  • dismiss — close the flag without touching the target.

A flag transitions to actioned on its first non-dismiss action, and closed is closed: re-POSTing a punitive action (hide, remove, warn, ban) to a resolved flag returns 409 FLAG_ALREADY_CLOSED so blind retries can't stack duplicate audit rows. The reversals — unhide, restore, dismiss — stay legal on a closed flag and each records another moderation_action row, so a moderator can undo a hide under the same flag thread.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "action": "dismiss",
    "notes": "Reviewed — content is on-topic."
  }' \
  https://social.productcraft.co/v1/workspaces/<ws>/communities/<c>/moderation/flags/<flag-uuid>/actions
response.json
{
  "flag": { "status": "dismissed", ... },
  "moderation_action": {
    "id": "7c4461e6-4904-4627-ad3e-ec80212b0f5a",
    "community_id": "<c>",
    "flag_id": "<flag-uuid>",
    "target_kind": "post",
    "target_id": "<post-uuid>",
    "moderator_account_id": "<your-platform-user-id>",
    "action": "dismiss",
    "notes": "Reviewed — content is on-topic.",
    "created_at": "2026-05-02T19:36:06.506Z"
  }
}

5

Audit trail

Append-only record of every moderator action. Required permission: social.audit.read.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/workspaces/<ws>/communities/<c>/moderation/audit?limit=50"

Wrap-up

One stage to go

The moderation loop closes the public-content story: actors, posts, comments, reactions, follows, mutes, blocks, feeds, stories, polls, notifications, flags, audits. Stage 10 covers the private half — direct and group conversations. And the tutorial deliberately stays on the golden path; surfaces like search, bookmarks, hashtags, lists, and muted terms live in the single-feature guides, with the complete endpoint inventory on the API reference.

  • The console at console.productcraft.co/social gives your moderation team a UI on top of these admin endpoints. A community's Moderation tab is the queue — Open / Actioned / Dismissed, with per-flag Hide · Remove · Warn · Ban · Dismiss buttons (and Unhide / Restore on resolved ones). The audit log renders as Recent moderation actions on the Analytics tab. Moderators can also review DM content from Moderation → Direct messages (moderation), a read-only conversation lane (covered in stage 10).
  • When you're ready to wire actor identity into Auth (so end-user signin auto-creates an actor), pass app_id when creating a new community (the app must belong to the same workspace). Existing standalone communities keep working unchanged.