A House Divided A House DividedDocumentation
Changelog
Game Design/Platform

Moderator Accounts

Last updated 2026-08-20
Source files

Overview#

Moderator accounts are an intermediate auth role between admin and player. Admins can assign/remove moderator status from users. Moderators get their own panel at /moderator with a subset of admin functionality - currently limited to player management. The system is designed so additional tabs can be added to the mod panel with minimal effort.

Mod notes and audit logs are shared between panels: admins see everything moderators do, and both roles can write mod notes with automatic attribution.

Data Model#

User Document Changes#

The role field on User expands from "admin" | "player" to:

role: "admin" | "moderator" | "player";

The modNote?: string field is replaced with a structured array:

modNotes?: ModNote[]

Where ModNote is:

interface ModNote {
  authorId: ObjectId;
  authorName: string;
  authorRole: "admin" | "moderator";
  text: string;
  createdAt: Date;
}

Both admins and moderators can add notes. All notes display in both the Admin Panel and Mod Panel with attribution (e.g., "ModeratorX - 2026-04-11: Note text").

New Collection: modAuditLog#

Tracks every privileged action a moderator takes. Admin actions are NOT logged here - this exists specifically for mod accountability.

interface ModAuditLogEntry {
  _id: ObjectId;
  moderatorId: ObjectId;
  moderatorName: string;
  action: string; // e.g. "ban_user", "unban_user", "add_mod_note", "grant_resources"
  targetUserId?: ObjectId;
  targetUsername?: string;
  details?: string; // human-readable context, e.g. "Banned for multiboxing"
  createdAt: Date;
}

No separate collection for moderator roster - moderators are queried via { role: "moderator" } on the users collection.

Auth System#

New Auth Guard: requireModerator()#

Location:

New Server Component Helper: getAuthModerator()#

Location:

JWT Changes#

No structural changes. The role field is already carried in the JWT payload via userPayloadSchema. The expanded role union is sufficient.

Existing Guards Unchanged#

Target Protection#

Mod-facing API routes that target a user must check the target is not an admin:

const targetUser = await db.collection<User>("users").findOne({ _id: new ObjectId(targetUserId) });
if (targetUser?.role === "admin") {
  return NextResponse.json({ error: "Cannot perform actions on admin accounts" }, { status: 403 });
}

This check lives in each mod API route, not in the auth guard (the guard checks who you are, the route checks who you're targeting).

Moderator Panel#

Route Structure#

Tabs#

Only one tab for now: Players. Adding more tabs later means adding entries to the mod tab config and ensuring API routes accept moderator auth.

Players Subtabs#

Subtab Component Strategy Differences from Admin
Users Reuse/adapt admin UsersTab Admin accounts hidden from list, no account deletion button
Grant Resources Reuse ResourceGrantManager No differences
Achievements Reuse AchievementsTab No differences
Characters Reuse CharacterStateManager No differences
Patreon Reuse PatreonManagementTab No differences
Activity Log Reuse ActivityLogTab No differences
Suspicious Reuse SuspiciousActivityTab No differences

Component Reuse#

Most admin player components can be reused directly. Where behavior differs (delete button visibility, admin account filtering), pass a context: "admin" | "moderator" prop. This avoids forking entire components while allowing targeted differences.

API Routes#

Moderator Routes (/api/moderator/)#

Route Method Purpose Audit Logged
/api/moderator/users GET List users (excludes admin accounts) No
/api/moderator/users/ban POST Ban a user Yes
/api/moderator/users/unban POST Unban a user Yes
/api/moderator/users/mod-notes POST Add a mod note Yes
/api/moderator/resources/grant POST Grant resources to a player Yes
/api/moderator/achievements GET List achievements No
/api/moderator/achievements/grant POST Grant achievement Yes
/api/moderator/characters GET List/view characters No
/api/moderator/characters/update POST Update character state Yes
/api/moderator/patreon GET View Patreon info No
/api/moderator/patreon/update POST Update Patreon tier Yes
/api/moderator/activity-log GET View activity log No
/api/moderator/suspicious GET View suspicious activity No

Every mutating mod route follows this pattern:

  1. Call requireModerator()
  2. Validate request body with Zod
  3. Check target user is not an admin
  4. Perform the action
  5. Write entry to modAuditLog
  6. Return response

Admin-Only Moderator Management Routes (/api/admin/moderators/)#

Route Method Purpose
/api/admin/moderators GET List all moderators
/api/admin/moderators/assign POST Assign moderator role to a user
/api/admin/moderators/remove POST Remove moderator role (set back to "player")
/api/admin/moderators/audit-log GET View mod audit log (filterable, paginated)

These use requireAdmin(). Moderators cannot assign/remove other moderators.

Admin Panel Changes#

New Subtabs under Admin > Players#

Moderators subtab:

Mod Audit Log subtab:

Profile Badge#

Admin Badge (existing, unchanged)#

Migration#

Existing modNote Field#

Users with an existing modNote string value need migration to the new modNotes[] array format. Migration script should:

  1. Find all users where modNote exists and is non-empty
  2. Convert each to a single-entry modNotes array: { authorId: adminId, authorName: "System (migrated)", text: modNote, createdAt: user.updatedAt }
  3. Unset the old modNote field

No users need role migration - all existing users remain "admin" or "player" as-is.

Gameplay Impact#

None. Moderator status has zero effect on gameplay mechanics - elections, legislation, campaigns, NPP behavior, etc. are completely unaware of the moderator role. Mods play the game as normal players with their characters.

Extensibility#

Adding new tabs to the mod panel in the future:

  1. Add tab entry to the mod panel tab config
  2. Create or reuse the component
  3. Add corresponding /api/moderator/ routes with requireModerator() guard
  4. No auth system changes needed

Connected pages

References →
wikiNPP Behavior
← Referenced by
None