← All entries

Dev Log

Build notes from the Jefe ecosystem

Jefebot: Twitch Token Auto-Refresh System

The Bot Whisperer 2026-02-25

The bot kept forgetting its own credentials. Replaced Jefebot's manual Twitch OAuth flow with a fully automatic token refresh system. The bot's EventSub kept dying with "subscription missing proper authorization" because the implicit grant flow provided no refresh token, forcing manual browser re-auth every few hours. Now tokens persist to disk and auto-refresh before expiry.

The Root Cause

The token generator (generate-twitch-token.js) was using the OAuth implicit grant flow (response_type=token), which only returns an access token with no refresh capability. Every time it expired, someone had to open a browser, re-authorize, copy-paste the token into .env, and restart the bot. For a bot that's supposed to run unattended, this was a non-starter.

New Architecture

A new centralized twitchAuthService manages the entire token lifecycle. It loads tokens from .twitch_tokens.json on startup, checks expiry, and auto-refreshes via the refresh_token grant 5 minutes before the token dies. Every Twitch API call in twitchEventSubService and twitchChatService now gets fresh tokens from this service instead of reading process.env directly. All API methods also got 401 retry logic: if a call fails with unauthorized, the service refreshes the token and retries once before giving up.

OAuth Flow Switch

The token generator now uses the Authorization Code flow (response_type=code), which requires a client secret but returns both an access token and a refresh token. On successful auth, it saves both to .twitch_tokens.json and updates .env for IRC fallback compatibility. Debugging the redirect URI mismatch was the real adventure here — turned out the .env had one Twitch app's client ID while the redirect URIs were being configured on a different app entirely. Classic multi-account mixup.

Housekeeping

While reviewing startup warnings, fixed two unrelated issues: triviascores.js was exporting a class instead of an instance (so the command handler couldn't find its execute method), and oceanmanor.js was a standalone feature class sitting in the commands/ directory where it didn't belong — moved it to services/.

What's Next

  • Monitor token refresh cycles in production to confirm the 5-minute buffer is sufficient
  • Verify EventSub subscriptions survive a full token refresh cycle without dropping channels