Reports
Feedback is not criticism — it is the system talking to itself.
A report is a signed-in record that one topic, one branch, or one user should be reviewed. Each user can file one report per target. Reports are not public — there is no .md, no .json, and no entry in the sitemap.
Why reports exist
Lorediv is built and maintained by its community. As the tree grows, some content will break the rules — spam, impersonation, misleading claims, or material that harms other users. The community needs a way to flag that content so moderators can act on it. Moderators can dismiss a false report, remove the offending topic or branch, or in the case of users who persistently violate good practices, close their account. The goal is to protect the integrity of the tree and the people who contribute to it, without silencing honest disagreement.
1. How it works
Report sits on the topic menu (⋯) and on the branch menu. If the menu is on a branch, the target is that branch. If it is on the topic, the target is the topic. The author of a topic does not see Report on that topic. The author of a branch does not see it on that branch. Report only appears for signed-in users — a guest does not see it.
flowchart TD
U["Signed-in user clicks Report"] --> D["Dialog"]
D --> R["Select reason<br/>Spam · Duplicate · Misleading<br/>Offensive · Other"]
R --> N["Optional note<br/>max 500 characters"]
N --> S["Confirm"]
S --> DB["Row stored<br/>status = open"]
DB --> W["Discord webhook<br/>if configured"]
The dialog requires one reason: Spam, Duplicate, Misleading, Offensive, or Other. A note is optional and stops at 500 characters. Nothing is stored until the user confirms. The menu then reads Reported and does not send a second report. A repeat from the server responds You already reported this.
Targets the API accepts
The menu only sends topic or branch. The API also accepts user if that user exists and is not the reporter.
| Target | Guard |
|---|---|
topic | Must exist. Cannot report your own topic if the reporter is the author (topics.user_id). |
branch | Must exist. Cannot report your own branch if the reporter is the author (branches.proposed_by). |
user | Must exist. Cannot report yourself if the target is the reporter. |
The author check on a branch uses the UUID proposed_by, not the visible author label.
2. What is stored
One row per user and target. The unique index is (reporter_id, target, target_id).
| Field | Value |
|---|---|
target | topic, branch, or user |
reason | spam, duplicate, misleading, offensive, or other |
note | Text, or null |
status | open at creation. Then dismissed or removed. |
reviewed_by, reviewed_at | Who closed the report and when. |
A second report from the same user on the same target is rejected at the database level — the unique index catches it before the application layer responds You already reported this.
Discord notification
After the row is stored, the system sends a webhook notification. The row stays even if the notification fails.
The webhook URL is set through an environment variable. When it is empty, the report is saved and no notification is sent. When it is set, it must be HTTPS on discord.com or discordapp.com, with a path that starts with /api/webhooks/. Timeout is 2.5 seconds. A failure is logged without killing the report.
The message includes: reason, target type, title, note, the reporter's handle (X first, then Google, then GitHub), a link to the topic, and a link to the review queue.
3. Who can review
A list of user IDs controls who can open the review queue. When that list is empty, the moderation route does not exist.
reports.access returns { allowed }. If it is true, the user's profile menu shows Moderation.
The queue
The queue lives at /moderation, outside the app group. Non-moderators receive the same result as an unknown route — notFound, not 403. robots.txt disallows /moderation. The page carries noindex.
The queue shows open reports, newest first, up to 100. Each row displays: reason, note, handle, target type, title, link to the topic, date, and how many open reports share that same destination.
flowchart TD
Q["Queue<br/>open reports"] --> D["Dismiss"]
Q --> R["Remove"]
D --> DF["Leaves the content<br/>closes that row<br/>status = dismissed"]
R --> RF["Deletes the topic or branch<br/>other open reports on that<br/>target pass to removed"]
RF --> P["Poll edge case<br/>removing a branch that leaves<br/>a poll at two options or fewer<br/>removes the whole topic"]
| Action | What happens |
|---|---|
| Dismiss | Leaves the content in place. Closes only that report row (dismissed). |
| Remove | Deletes the topic (same as author delete) or the branch (same rules as author delete). All other open reports on that target pass to removed. |
| Remove on a user | Blocked. A user report can only be dismissed. No accounts are deleted. |
Removing a branch from a poll that has two root options or fewer removes the whole topic — the same rule that applies when an author deletes a branch. This applies even when the branch is a reply.
4. Limits
Requires a session. Each operation spends two budgets: the action-specific budget and the shared IP write budget.
| Action | User budget | IP budget |
|---|---|---|
| Create a report | report:create — 5 per 10 minutes | write:ip — 60 per minute |
| Review a report | report:review — 30 per hour | write:ip — 60 per minute |
In production, without Redis, writes are closed. The /api/trpc gateway also charges query:read on these calls. reports.queue and reports.access only spend that gateway token.
There is no public .md or .json for a report. A report does not appear in the sitemap. The moderation page is not a public address on the tree.
Code
| Piece | Path |
|---|---|
| Router | apps/frontend/src/server/routers/reports.ts |
| Dialog | apps/frontend/src/components/ui/ReportDialog.tsx |
| Menu | apps/frontend/src/components/ui/ThreeDotMenu.tsx |
| Queue | apps/frontend/src/app/moderation/page.tsx |
| Queue UI | apps/frontend/src/components/moderation/ModerationQueue.tsx |
| Remove logic | apps/frontend/src/server/lib/moderate-target.ts |
| Discord webhook | apps/frontend/src/lib/discord-report.ts |
| Moderator check | apps/frontend/src/lib/moderator.ts |
| Table | packages/shared/src/schema/reports.ts |