Skip to content

BanEntry

The BanEntry table maintains a cluster-wide record of all player bans. Every ban — whether permanent or temporary, active or revoked — is stored as a row in this table. Game hooks subscribe to the full table to enforce bans in real time across all servers simultaneously.


Scope

🌍 Global — All game servers subscribe to the complete BanEntry table. When a ban is issued or revoked, every server in the cluster receives the update within milliseconds via SpacetimeDB's subscription push.


Schema

Column Type Constraints Description
ban_id u64 Primary Key, auto-increment Unique identifier for this ban record
player_id String Indexed Platform-wide player identifier of the banned player
player_name String Display name of the banned player at time of ban
reason String Human-readable reason for the ban
banned_by String Identifier of the admin who issued the ban
banned_at Timestamp When the ban was issued
expires_at Option<Timestamp> When the ban expires (None = permanent)
revoked bool Whether the ban has been manually revoked
revoked_by String Identifier of the admin who revoked the ban (empty if not revoked)
revoked_at Option<Timestamp> When the ban was revoked (None if still active)

Rust Definition

#[spacetimedb::table(public, name = ban_entry)]
pub struct BanEntry {
    #[primary_key]
    #[auto_inc]
    pub ban_id: u64,
    #[index(btree)]
    pub player_id: String,
    pub player_name: String,
    pub reason: String,
    pub banned_by: String,
    pub banned_at: Timestamp,
    pub expires_at: Option<Timestamp>,
    pub revoked: bool,
    pub revoked_by: String,
    pub revoked_at: Option<Timestamp>,
}

Usage Patterns

Ban Enforcement

Game hooks determine whether a player is banned by querying the subscribed BanEntry data. A player is considered actively banned when all of the following are true:

  1. A BanEntry exists with a matching player_id.
  2. revoked is false.
  3. Either expires_at is None (permanent) or expires_at is in the future.
// Pseudocode — game hook ban check on player connect
fn is_player_banned(player_id: &str) -> bool {
    BanEntry::filter_by_player_id(player_id)
        .any(|ban| !ban.revoked && !is_expired(&ban))
}

fn is_expired(ban: &BanEntry) -> bool {
    match ban.expires_at {
        Some(expiry) => Timestamp::now() > expiry,
        None => false, // Permanent ban — never expires
    }
}

Soft-Delete Model

Bans are never physically deleted from the table. Instead, revocation is recorded by setting revoked = true, revoked_by, and revoked_at. This preserves a complete audit trail — administrators can review a player's full ban history, including bans that were later overturned.

Multiple Bans Per Player

The index on player_id is a non-unique index, meaning a single player can accumulate multiple ban records over time. The enforcement logic must check all matching rows to determine current ban status.

Temporary Bans

When expires_at is set to a future timestamp, the ban automatically becomes inactive after that time. Game hooks are responsible for checking the expiry during connection attempts. No reducer or scheduled task is needed to "expire" bans — the expiry is evaluated at query time.


Reducer Operation
admin_ban Inserts a new BanEntry row
admin_unban Sets revoked = true on matching ban(s)
panel_ban_player Panel wrapper → admin_ban
panel_unban_player Panel wrapper → admin_unban

Subscriber Query Purpose
Game Hook SELECT * FROM ban_entry Real-time ban enforcement on all servers
Admin Panel SELECT * FROM ban_entry Ban management UI, player history

For full subscription architecture, see Subscriptions.