Sesame
Sesame is the core engine of ItsBagelBot. It consumes ingress events, evaluates gates, dispatches commands, runs module event handlers, and routes outputs to the outgress service.
Architecture
Section titled “Architecture”Sesame operates as a highly concurrent NATS JetStream consumer. It pulls from the twitch.ingress.event.* subjects across two lanes (Premium and Standard) and drains them into a shared pool with reserved capacity for Premium events.
The Pipeline
Section titled “The Pipeline”Every message flows through engine.Pipeline, an allocation-free (for non-emissions) decoding and dispatch stage:
- Decode: The JSON payload is unmarshaled into a pooled
lane.Envelope. - Eligibility: Unsupported events and envelopes without a valid broadcaster are discarded before projection or command work.
- Module Views: If the event requires configurable behavior, the
Projectoris queried for the broadcaster’sModuleViewset. - Command Dispatch: If the event is a chat message (
channel.chat.message), the pipeline checks the command registry. Baked commands are evaluated for their permissions, cooldowns, and live-only gates. - Event Handlers: Non-command event handlers registered by modules are executed in registration order.
- Emission: Module handlers do not publish directly to NATS. They yield an
Outputstruct to anEmitcallback, which builds theoutgress.Messagewire contract and publishes it to eithertwitch.outgress.premiumortwitch.outgress.standard.
Transport replay does not claim a Valkey key. Outputs derived from an input with a stable event ID use a stable publication ID and wait for the NATS confirmation before the input is acknowledged. Valkey remains domain state, not a second transport acknowledgement system.
Module Authoring
Section titled “Module Authoring”Sesame’s feature set is authored in the module package. To ensure high testability and to keep the authoring surface completely free of runtime wiring (like Valkey, Projector, or NATS), features are declared using a fluent builder pattern.
A module is instantiated, its commands and event handlers are chained, and it returns an immutable Module that the engine.Registry consumes at startup.
The Fluent Builder
Section titled “The Fluent Builder”Here is an example of authoring a module using the builder:
func MyModule() module.Module { // 1. Initialize with name and kind (Core, Default, Opt-In) m := module.NewModule("my_feature", module.KindDefault)
// 2. Register non-command EventSub handlers m.On("channel.chat.message", handleChat) m.On("stream.online", handleStreamOnline)
// 3. Declare commands with chained gates m.Command("ping").Everyone().Run(pingRun) m.Command("announce").Mod().Cooldown(10 * time.Second).Run(announceRun) m.Command("shoutout").Aliases("so").Mod().LiveOnly().Run(soRun)
// 4. Validate and build the immutable artifact return m.Build()}Module Kinds
Section titled “Module Kinds”Modules are declared as one of three kinds, dictating their enablement logic:
- Core: Always enabled, never toggled, skips projection fetches.
- Default: Enabled by default, can be disabled via the dashboard.
- Opt-In: Disabled by default, must be explicitly enabled via the dashboard.
Command Gates
Section titled “Command Gates”The .Command("name") method returns a CmdBuilder allowing you to seamlessly chain execution gates before finalizing with .Run().
- Permissions:
.Everyone(),.Sub(),.VIP(),.Mod(),.Broadcaster() - State:
.LiveOnly(),.Cooldown(time.Duration) - Routing:
.Aliases("trigger"),.NumericSuffix()(absorbs trailing digits inline, e.g.!clip30resolves toclip).
The engine.Registry indexes these built modules at startup, constructing a flat, case-insensitive command index and a routing table for event types.
Variables & Templating
Section titled “Variables & Templating”Sesame provides a fast, allocation-light string templating system used for dynamic command replies. The module.Expand (and module.ExpandString) functions parse strings for {key} tokens and resolve them using a provided callback.
The system supports passing through literal {key} tokens if the callback does not recognize them, instead of silently dropping them.
Additionally, the module.ParseDynamic helper provides built-in support for generic dynamic variables:
{random}: Generates a random number between 1 and 100.{random:min-max}: Generates a random number betweenminandmax.{choice:a,b,c}: Picks a random string from a comma-separated list of choices.
State & Caching
Section titled “State & Caching”Sesame maintains high throughput by avoiding database reads on the hot path:
- Projection Cache:
projection.Readerprovides an in-memory cache of broadcaster settings (modules, users, custom commands), falling back to NATS RPC (bagel.rpc.internal.projection.*) and listening forbagel.cache.invalidate.*broadcasts. - Live Store:
ValkeyLiveStorechecks if a broadcaster is currently live. It caches locally, falls back to Valkey, and can trigger a system outgress lane check to Twitch if the key is cold. - Domain State: Cooldowns, timers, loyalty live views, greeting windows, and reputation state are backed by Valkey. Transport replay is not.
Follow-alert dedupe
Section titled “Follow-alert dedupe”Twitch re-sends channel.follow on every re-follow, so an unfollow/refollow loop would otherwise drive the chat
alert as fast as a viewer can click. The alerts module claims alert:follow:<broadcaster>:<follower> for 72 hours
using the same SET key 1 NX PX idiom as a command cooldown, and posts the thank-you only when it wins the claim.
Both halves of the key are Twitch numeric IDs, so a rename cannot reopen the window, and one channel’s claim never
suppresses another’s. The claim is taken after the enable toggle is read, so a channel with follow alerts off never
burns a window it would want the moment it turns them back on.
Only follows are gated. Subs, gifts, cheers and raids each cost the sender something, and ad breaks are the channel’s own event.
Three properties of the fleet make a multi-day window safe to hold in Valkey:
- The claim always lands on the primary.
SET ... NXis a write, so replica lag cannot let two replicas both believe they won the window. - The window survives restarts. The keyspace runs AOF with
appendfsync everysecon a persistent volume (save "", no RDB snapshots). The AOF records expirations as absolute timestamps, so a pod restart or a full fleet restart restores each claim with its remaining time, not a refreshed 72 hours. The only loss window is the sub-second of writes an unclean kill can drop, plus the few milliseconds of unreplicated writes a Sentinel failover discards. Both cost at most a duplicate alert. - The key set stays small. At 5,000 follows/day a channel holds roughly 15,000 claims of about 120 bytes each:
under 2 MiB against the 512 MiB
maxmemorybudget. The window could be raised to a week at the same order of cost. The pressure to watch is not size but policy:maxmemory-policy volatile-lruevicts TTL-carrying keys first, and these claims are written once and rarely read, making them the coldest keys in the keyspace. Sustained memory pressure would evict them and reopen alerts.
The gate fails open. With Valkey unreachable the alert still posts and the miss is logged, because a lost thank-you is a worse outcome than a duplicate, and the abuse the gate exists for cannot cause the outage.