DOCS/STATE.MD

Per-Guild State and Persistence

One GlassFrame instance can serve many Discord servers at once. Three things are tracked independently per server, and all three survive a bot restart if you give GlassFrame a persistent stateStore:

  • Which layers are on. Server A can run AntiRaid + AntiNuke while Server B runs nothing, or everything, independently. There is no such thing as "GlassFrame is on" globally - only "layer X is on for guild Y."
  • The whitelist. Exempting a user in one server does not exempt them anywhere else.
  • The command prefix (v2.1.0+), if a server has set its own via !gf prefix set. config.prefix is still the fallback everywhere a server hasn't customized it.

Everything else that's naturally guild-scoped already was before this - threat scores, open cases, join-rate tracking, audit-log burst tracking are all keyed by guildId internally. The two items above are the ones that used to be accidentally global; see CHANGELOG.md for the 2.0.0 entry if you're upgrading from an earlier version.

How it's stored

Layer.enabledGuilds is a Set<guildId> per layer (not one shared boolean). GlassFrame.whitelist is a Map<guildId, Set<userId>>. Both are read through the frame's own methods rather than touched directly:

frame.enableLayer("antiRaid", guildId);
frame.disableLayer("antiRaid", guildId);
frame.getStatus(guildId); // { basicSecurity: true, antiRaid: false, ... }

frame.addToWhitelist(guildId, userId);
frame.removeFromWhitelist(guildId, userId);
frame.isWhitelisted(guildId, userId);

Persistence across restarts

Pass a stateStore when constructing GlassFrame - MemoryStateStore (default, lost on restart) or JSONFileStateStore (a single JSON file, no database, ARM64/Termux-friendly):

const { JSONFileStateStore } = require("glassframe-protocol");

const frame = new GlassFrame(client, {
  getLogChannel: /* ... */,
  stateStore: new JSONFileStateStore("./glassframe-state.json"),
  autoStart: ["basicSecurity", "antiRaid"]
});

Every time a layer is toggled or the whitelist changes for a guild, GlassFrame writes that guild's full state (which layers are on, its whitelist) to the store. At startup, for every guild the bot is already in, and again automatically whenever the bot joins a new guild (guildCreate), GlassFrame:

  1. Checks the store for saved state for that guild.
  2. If found, restores exactly that - the layers that were on stay on.
  3. If nothing is saved yet (a guild GlassFrame has never seen before), applies autoStart and the constructor's whitelist option (see below) as that guild's starting defaults.

Restoring is async (a real stateStore might read from disk or a database), so it can't finish before the constructor returns. In practice this resolves well before a Discord event could possibly arrive, so most code never needs to think about it - but if you want a guarantee, await frame.ready after construction; it resolves once every guild the bot was already in has been restored (guilds joined later via guildCreate restore independently and aren't part of this promise, since there's nothing they could race against).

autoStart and the whitelist option are only ever defaults for a guild's first run - once a guild has any saved state (even "everything off"), neither applies to it again; the saved state is the source of truth from then on.

The whitelist constructor option

new GlassFrame(client, { getLogChannel: /* ... */, whitelist: ["123456789012345678"] });

This seeds a starting whitelist for every guild - both the ones already in client.guilds.cache and any the bot joins later - the first time GlassFrame sees that guild (i.e., it never overrides a guild's own saved whitelist). Think of it as "these users are trusted everywhere by default"; frame.removeFromWhitelist(guildId, userId) still works normally per-guild afterward.

Writing your own store

Implement two async methods and pass an instance as stateStore:

class MyStore {
  async get(guildId) { /* return the saved state object, or null */ }
  async set(guildId, state) { /* persist `state` for this guildId */ }
}

state is { layers: { basicSecurity: true, antiRaid: false, ... }, whitelist: ["userId1", "userId2"], prefix: "!custom " } - prefix is null for a server using the default.