Skip to content

Architecture

Jason Tucker edited this page Jun 5, 2026 · 2 revisions

Architecture

How it runs

Developer PC
  └─ git push to GitHub main

GitHub Actions (7 GB RAM runner)
  ├─ docker build  → TypeScript compiles INSIDE the multi-stage image (tsc, builder stage)
  ├─ push image    → ghcr.io/jason-tucker/squishybot:{sha, latest}
  ├─ run the built image once → node dist/bot/registerCommands.js  (deploy slash commands)
  └─ SSH to VPS    → git reset --hard origin/main && docker compose pull && up -d

VPS
  ├─ watchtower    → polls :latest (~30s), restarts the container when the digest changes
  ├─ entrypoint    → drizzle-kit push --force  (syncs schema to Postgres on every start)
  └─ node dist/index.js   ← bot runs from compiled JS

Two things deploy a new image to the VPS: the CI SSH step (fast path) and watchtower (polls :latest). The container carries com.centurylinklabs.watchtower.enable="true".

Key constraint

The VPS has ~900 MB free RAM. tsc OOMs it. Never run pnpm build / tsc on the VPS. TypeScript compiles only in the Docker builder stage (with --max-old-space-size=4096) on the CI runner; the production stage copies dist/ + node_modules and runs node dist/index.js.

Boot sequence (clientReady)

src/bot/events/ready.ts:

  1. loadSettings() first (so presence can read persisted state), then initPresence.
  2. In parallel: loadGames, loadSocialFeeds, resolve bot owners.
  3. Start the schedulers: birthday scheduler, social poller, game auto-archiver, reaction-role cache + cleanup.
  4. Redis fan-out: publishReady + a 60s publishHeartbeat on bot.squishy.bot.*.
  5. runReconciler() — repair voice state (see below).
  6. startRpcServer() (botpanel command bus) and startCacheInvalidator() (cache-reload subscriber).
  7. DM the bot owner a Components V2 boot card (version, build SHA, reconciler stats, disabled feature flags).

Voice services

Service File What it does
Hub Manager services/voice/hubManager.ts Detects hub joins, renames the hub in place, creates the replacement hub, seeds hubs from env
Auto Channel services/voice/autoChannel.ts Creates/deletes the voice+text pair, sets permission overwrites, applies per-hub defaults
Auto Naming services/voice/autoNaming.ts Presence-driven rename (every member counts, not just the owner); shared by presenceUpdate, the Auto/Counter templates, and reconciler retry
Control Panel services/voice/controlPanel.ts Posts/updates the Components V2 panel
Sticky services/voice/sticky.ts Keeps the 📋 Open Panel button pinned at the bottom of the text channel
Cleanup Scheduler services/voice/cleanupScheduler.ts DB-backed timers; deletes empty channels after the configured delay
Owner Grace services/voice/ownerGrace.ts Grace window + acting-owner promotion when the owner leaves
Hub Lockdown services/voice/hubLockdown.ts Per-hub + server-wide Connect kill switch with restore-on-boot
Hosts Service services/voice/hostsService.ts Add/remove hosts (shared by slash + RPC)
Permissions services/voice/permissions.ts isSudo, isOwner, isHost, syncTextChannelPermissions
Reconciler services/voice/reconciler.ts Startup repair (below)

Other services

Service File What it does
Settings services/settings.ts bot_settings cache with env fallback; getSetting / getBoolSetting / setSetting
Games services/games.ts Game catalog cache; pref apply/sync
Birthday scheduler services/birthdayScheduler.ts Per-minute wall-clock check; daily idempotent ping
Social poller services/social/poller.ts + rssParser.ts Fetch/dedupe/repost RSS feeds (~30 min)
Reaction roles services/reactionRoles.ts Watched-message cache + expiry cleanup
Game auto-archive services/gameAutoArchive.ts Archives stale game channels
Bot owner services/botOwner.ts Dynamic owner resolution from the dev-portal Team (see Bot Owner)
Presence services/presence.ts Bot status driven by activity/errors
Logger services/logger.ts Console + DM to the bot owner on errors
Event bus services/eventBus.ts Redis publish on bot.squishy.* (ready/heartbeat/voice/member events)
RPC server services/rpcServer.ts + rpc/ botpanel command bus subscriber (below)
Cache invalidator services/cacheInvalidator.ts Reloads caches on HMAC-signed bot.squishy.settings.invalidate events

Reconciler

runReconciler() is the self-repair pass run on every boot:

  1. Seed hubs from HUB_CHANNEL_IDS env.
  2. For each auto_channels row (bounded concurrency): verify the VC exists; clean up orphans (delete the text channel + row + member rows); otherwise re-sync text-channel permissions, rebuild the panel + sticky, and sweep stale panel messages.
  3. Backfill auto_channel_members for currently-occupying members, and re-run auto-rename where the owner is present and playing.
  4. Restore in-flight cleanup timers, owner-grace windows, and hub lockdowns.

Untracked channels inside the auto-voice category are logged only — never auto-adopted (a previous version over-reached and swept up manually-created channels).

botpanel integration (Redis)

The bot exposes most flows as RPC verbs so the botpanel web dashboard can drive them:

  • Command busstartRpcServer psubscribes to cmd.squishy.*. Each envelope is HMAC-signed with BOTPANEL_RPC_SECRET; bad signatures drop with a warn. Handlers live under services/rpc/handlers/ (voice rename/lock/hide/transfer/delete, hosts, hubs lockdown, games provision/set_prefs, discord create_role/channel, play.post, report.submit, color.assign, reaction-role create/delete/expire, staff grant/revoke, users.resolve, meta pickers, admin reconciler/orphan-scan/reload-caches). Each verb delegates to the same service helper the slash flow uses, so the two surfaces stay byte-identical.
  • Cache invalidatestartCacheInvalidator subscribes to bot.squishy.settings.invalidate; on a verified event it reloads the matching cache (settings, reaction roles, …) so panel edits take effect without a restart.
  • Event buspublish* on bot.squishy.* emits ready/heartbeat plus voice/member lifecycle events the panel consumes.

All three are optional: with BOTPANEL_RPC_SECRET unset or Redis down, the bot logs a warning and runs normally.

Schema-change notification

A push to main that touches src/db/schema/** fires a repository_dispatch (bot-schema-changed) at botpanel, whose companion workflow re-vendors the Drizzle schemas — closing the race where the panel's main could go red after a schema merge here.

Tech stack

Layer Tool
Language TypeScript (strict)
Runtime Node 24 — node dist/index.js in Docker
Discord discord.js v14 (Components V2)
Database PostgreSQL 16 + Drizzle ORM
Schema drizzle-kit push (no SQL files in git)
Cache/bus Redis (ioredis) — optional
Env Zod-validated .env
CI/CD GitHub Actions → GHCR → watchtower

Clone this wiki locally