Overview
What this page covers
The bot page covers what apoptotic does. This page
covers how — the architecture choices that make it a single Rust binary
instead of an ecosystem of bots, the persistence design that lets it serialize
every write naturally, and the security model behind the cross-service
/verify flow it shares with SP-Legends
and the apex domain at apoptoses.com.
Stack
One Rust binary
apoptotic is built on top of serenity for the Discord gateway
and poise for slash-command routing. Both are mature Rust
crates with a sensible split between the low-level Discord protocol and the
high-level command framework — using them together gives a strongly typed
surface for the parts of the bot that matter most (commands, permissions,
context types) without losing access to the raw gateway when an event handler
needs it.
On top of that the bot runs an Axum HTTP callback server. This
is the channel through which the apex website's /verify endpoint
posts back hashed identifiers, and through which the
dashboard queries and mutates configuration
via an internal REST API. Everything lives in one binary, on one VM, with one
unit file.
Persistence
The dedicated SQLite worker thread (db_task)
The bot's persistence layer follows a deliberate pattern that I now use across
several services: a single OS thread owns the
rusqlite::Connection, and async code interacts with it through an
mpsc channel with oneshot replies. The pattern lives in
dc_bot/src/db_task.rs and is replicated almost verbatim in
emu_service.
The benefits compound:
- Writes serialize naturally. There is no contention because there is exactly one writer.
- Tokio worker threads never block on disk. The async side only blocks on the channel, which is cheap.
- Connection state is local. Prepared statements, in-flight transactions, and pragmas live with the worker — no
Mutex<Connection> dance.
- Migrations and integrity checks run at startup in the same thread that will own the connection for the rest of the process's life.
The pattern is not novel, but the consistency of using it across every service
that touches SQLite means a single mental model covers all of them.
Features
Feature surface
The bot bundles the usual community-server feature set — moderation, AutoMod,
event logging, leveling, channel games, voice, scheduled tasks — and a few
less-common ones: opt-in alt detection through the apex website's hashed-IP
/verify handshake, Roblox player and private-server integration via
SP-Legends, and Buy Me a Coffee membership-tier role sync. The full breakdown
is on the bot page; the
command reference lists every slash command.
Security
Cross-service verification (/verify)
Verification is the one place where the bot needs to talk to the user's browser
rather than just to Discord. The flow is:
- A server admin runs
/verify in their guild. The bot generates a single-use, time-limited token, signs it with HMAC, and DMs the user a link to apoptoses.com/verify?token=....
- The user clicks the link. The website validates the HMAC and the token's expiry, and computes a salted SHA-256 hash of the visitor's IP address.
- The website POSTs the hash and the Discord user ID — and only those — back to the bot's HTTP callback server.
- The bot stores the hash. Two users in the same guild whose hashes collide are flagged for review as possible alts; the raw IP is never stored or transmitted.
The token is single-use; reused tokens are rejected. The hash is salted per
guild, so the same IP across two different guilds produces two different hashes
and cannot be cross-correlated. The full data-handling description is in the
apex Privacy Policy.
Operations
Where it runs
The bot runs on its own dedicated Debian VM, supervised by a systemd unit, with
journal-only logs locally and structured aggregation through the shared
log_store library where appropriate. Restarts are graceful: the
gateway connection is allowed to drain, scheduled tasks replay from SQLite on
boot, and any in-flight HTTP callbacks fail closed rather than open.
Deployment is handled through the local admin desktop app —
the same egui-based tool that pushes main_site and
spl_site. Source is tarred (excluding target/,
.env, and certs), uploaded by SCP, built on the VM, and the
binary swap is followed by a systemctl restart.
Lessons
What I'd do the same again
- Single binary per service. Operating one process per VM is dramatically simpler than orchestrating several.
- Dedicated DB worker thread. The serialization-by-construction property is worth more than the small amount of message-passing overhead.
- Hash, don't store. The
/verify flow could have stored raw IPs to make alt detection easier; not storing them at all is a simpler privacy story for everyone.
- Web UI + slash commands as peers. Some admins prefer typing; some prefer clicking. The dashboard means neither group is the second-class citizen.
Related
Read more