Documentation menu

Cloud Bans

A shared, customer-vouched community ban pool for Citadel Cloud subscribers. The premise: every Cloud-protected server has independent operators dealing with the same handful of serial cheaters and griefers. Cloud Bans makes those bans collective so each operator only has to do the work once.

This page documents the data model, trust-and-safety policy, customer experience, and the appeal flow. If you just want the what and why, the Cloud product overview is the right starting point — this page is for operators who want to understand what they're opting into.

Mental model

Three independent pieces:

  • Customers — Cloud subscribers. Each has a vouch_weight that starts at 1.0 and rises or falls based on submission/overturn history.
  • Submissions — a customer attaching a SteamID to a reason category. A submission is a vouch of weight equal to the customer's current vouch_weight.
  • Community bans — pool entries keyed by SteamID. A ban accumulates vouch weight from independent submissions until it crosses a threshold and goes live.

A pool entry is active (propagating to all subscribers) only after independent customers vouch for it with cumulative weight ≥ a configurable threshold. Until then it sits as pending and isn't visible to other subscribers' /sync queries.

The point: a single bad-faith customer can't get someone banned across the network alone.

The reputation model

vouch_weight

Every customer starts with vouch_weight = 1.0. It changes based on history:

  • Each successful overturn of a ban you submitted multiplies your weight by an overturn penalty (default 0.7). Three overturns drops you to 0.343 — meaning you'd need 9+ co-signers per submission to hit the activation threshold, instead of 3.
  • High overturn rate triggers a hard lock. If your overturn rate over the last N submissions (default 20) crosses a threshold (default 30%), your weight is clamped to 0 and submissions stop counting until a moderator manually resets you.

This isn't moralistic — it's purely mechanical. A customer who submits ten valid bans for every one wrong call retains full weight. A customer with consistently bad bans loses influence on the network.

Activation threshold

A pool entry flips from pending to active when its accumulated vouch_weight_total reaches the configured vouchThreshold (default 3.0). With everyone at the default vouch_weight = 1.0, that's three independent customers vouching. After overturns, it's higher.

High-impact manual review

A pool entry that accumulates more than the highImpactThreshold (default 50 vouches) is flagged with manual_review_required = true and held back from /sync until a moderator approves. This catches the worst-case scenario of "everyone copied the same wrongful ban" before it propagates further.

This mostly never fires in practice — most legitimate bans top out at 5–10 vouches. The threshold is a circuit breaker for the rare case where a popular but-wrong ban goes viral.

Expiry

Bans auto-expire after expiryMonths of no fresh submissions (default 12). The intent is to age out historic bans on accounts that may have been resold, recovered, or otherwise legitimately changed hands. A fresh submission resets the expiry clock.

All knobs

ConfigDefaultEnv var
vouchThreshold3.0CLOUD_BANS_VOUCH_THRESHOLD
overturnPenalty0.7CLOUD_BANS_OVERTURN_PENALTY
overturnRateLockThreshold0.30CLOUD_BANS_OVERTURN_RATE_LOCK
overturnRateWindow20CLOUD_BANS_OVERTURN_WINDOW
highImpactThreshold50CLOUD_BANS_HIGH_IMPACT_THRESHOLD
expiryMonths12CLOUD_BANS_EXPIRY_MONTHS
rateLimit24h50CLOUD_BANS_RATE_LIMIT_24H
rateLimit30d1000CLOUD_BANS_RATE_LIMIT_30D

The defaults are calibrated for the current customer base. We'll publish updates here if we tune them after seeing real submission patterns at scale.

Customer flow

Submitting a ban

When you ban a SteamID locally with the Submit to Cloud Bans checkbox enabled, the dashboard calls:

POST /api/v1/cloud-bans/submit
{
  "steamId": "76561198012345678",
  "reasonCategory": "cheating",   // 'cheating' | 'griefing' | 'exploiting' | 'other'
  "notesLocal": "free-text — optional, only stored locally with your submission, never shared"
}

The endpoint:

  1. Verifies your license token + active Cloud subscription.
  2. Checks rate limits (50/24h, 1000/30d per customer by default).
  3. Re-fetches your current vouch_weight (refuses if zero — see lockouts below).
  4. Creates or updates the pool's community_bans entry, adds your ban_submissions row, recomputes the threshold.
  5. Returns the resulting state: pending (if threshold not yet hit) or active (propagated).

If your customer-side vouch_weight is locked to zero (overturn-rate trip), the call returns 403 with a message pointing to the appeal/moderator-review path. You can keep banning locally — Cloud just ignores the submission.

Receiving bans

Your local install polls /api/v1/cloud-bans/sync periodically (cursor-paginated by updated_at):

GET /api/v1/cloud-bans/sync?since=2026-05-07T08:00:00Z&limit=500

Returns a page of community bans whose status is active, overturned, or expired. Your local cache adds the actives and removes the overturns/expires. Pending bans never show up here — they haven't earned propagation yet.

For just-in-time checks at player connect time:

GET /api/v1/cloud-bans/check?steamId=76561198012345678

Returns { banned: true, reasonCategory, vouchCount, activatedAt } or { banned: false }.

Unenrolling a ban

If you change your mind about a ban you submitted (e.g., you discover the player was framed):

POST /api/v1/cloud-bans/unenroll
{ "steamId": "76561198012345678" }

This marks your submission as withdrawn and recomputes the pool entry's threshold. If your withdrawal drops the weight below the activation threshold, the ban flips back to pending and stops propagating to new sync requests. Existing sync recipients pick up the change as overturned on their next page.

Unenrolling on a ban that hasn't been independently vouched yet effectively retracts it. Unenrolling on a ban that ten other customers also vouched leaves the ban active — you've just removed your contribution.

Appeals — the public path

Players can appeal through the public form at /appeal (see the Trust Network overview for the operator-facing summary). Anyone (including non-customers — typically the banned player themselves) can file an appeal for a SteamID they believe was wrongly banned. The endpoint is unauthenticated and per-IP rate-limited to discourage spam:

POST /api/v1/appeals
{
  "steamId": "76561198012345678",
  "appellantEmail": "[email protected]",
  "reason": "I was using legitimate keybinds, not autohotkey...",
  "evidence": "https://youtube.com/watch?v=..."
}

The endpoint returns a tracking token the appellant can use to check status:

GET /api/v1/appeals/:appellantToken

Anti-enumeration: appeals against SteamIDs that aren't actually community-banned still return a fake-looking tracking token. Otherwise the public endpoint would be a way to enumerate the ban list by trial-and-error.

Moderators review appeals in a queue and decide:

  • Overturned — ban reversed; propagates as overturned in /sync so all subscribers remove their local copy. Each contributing customer's vouch_weight is multiplied by the overturn penalty (default 0.7).
  • Upheld — appeal denied; the ban stays active. No customer-side weight change.
  • Dismissed — insufficient information to act. No-op; appellant can submit a new appeal with more evidence.

The appellant gets an email at every step (received, decided), and decisions land in their tracking page.

Audit and moderation

The full Cloud Bans story is recorded in two audit logs:

  • audit_log (the cloud-side table, separate from the controller-side audit log) — records every submission, unenroll, appeal, moderation decision, and customer reputation event. See audit log codes for the action strings.
  • Customer reputation history — every change to a customer's vouch_weight and lockout state, with reason. Surfaces in the admin dashboard.

Moderators have a batch-overturn endpoint (/api/v1/admin/cloud-bans/batch-overturn) for the case where a single bad-faith customer's submissions need to be reversed at once.

Privacy carve-out

Citadel's normal "nothing leaves your machine" promise gets a specific, narrow carve-out for Cloud Bans:

What's submitted to citadels.cc:

  • The SteamID being banned.
  • The reason category (cheating | griefing | exploiting | other).
  • Your account's vouch_weight at submission time (recorded with the submission so the threshold math is reproducible).

What's NOT submitted:

  • Player names.
  • Server names, IPs, or addresses.
  • Mod lists or server configs.
  • Chat logs.
  • The notesLocal free-text field — that stays on your install only.
  • Any data about non-banned players.

The data model is intentionally minimal. The minimum signal needed to do useful cross-customer protection is the SteamID + reason + the submitter's reputation; everything else is local concern.

If you'd rather not participate in Cloud Bans even with a Cloud subscription, the Submit to Cloud Bans checkbox is unchecked by default for new bans. You can subscribe to Cloud, use other features (when they ship), and never submit a single SteamID to the pool.

Public stats

The /api/v1/cloud-bans/stats endpoint is unauthenticated and returns aggregate-only numbers for the citadels.cc/cloud marketing page:

{
  "activeBans": 1247,
  "bansActivatedThisWeek": 38,
  "contributingCustomers": 142
}

No per-customer or per-SteamID data is exposed. The customer count is rounded to "how many distinct customers have at least one non-unenrolled submission ever" — it's a participation metric, not a leaderboard.

Operational notes

  • The trust-and-safety logic lives in packages/api/src/lib/cloud-bans-reputation.ts. Route handlers must go through it; bypassing it would skip the rate limits, weight updates, and audit writes.
  • Submission endpoints are rate-limited per-customer inside the reputation module; the global Fastify rate limit on the route scope is just network smoothing on top.
  • /api/v1/appeals is per-IP rate limited (10/min) to discourage moderation-queue spam.

Next