Better Stack Status Bot connects a Better Stack status page to Discord. It announces every new incident and status update in the channel you choose, and it answers a /status command with the live uptime of each service on your page.
The bot is a single Rust binary with a small SQLite file next to it. It needs no web server, no database server, and no hosting account beyond somewhere to run a process.
Aster Privacy built it to keep the Aster Mail community informed, and it works with any Better Stack status page.
- Announces incidents. The bot reads your status page RSS feed once a minute, posts each new entry in your updates channel, and adds a button that links back to the status page. It remembers what it has already posted, so a restart never repeats an announcement.
- Mentions a role. Each announcement can ping a role you nominate, so people opt in to status pings instead of receiving every one.
- Reports live status. The
/statuscommand reads the Better Stack API and replies with an embed listing every service, its current state, and its uptime percentage. The reply is ephemeral, so it stays visible only to the person who ran it.
| Command | Who can run it | What it does |
|---|---|---|
/status |
Anyone in the server | Shows every service on the status page with its state and uptime |
~register_commands |
Application owner and anyone in BOT_OWNERS |
Registers the slash commands with Discord. Run this once after you invite the bot |
~ is the default prefix for text commands. To change it, set COMMAND_PREFIX.
- Rust 1.95 or later, or Docker
- A Discord application and bot token
- A Better Stack status page, an Uptime API token, and the status page ID
- Open the Discord Developer Portal and select New Application.
- Go to Bot and select Reset Token to reveal a token. Copy it, because Discord shows it only once.
- On the same page, turn on Message Content Intent under Privileged Gateway Intents. The bot needs it for the
~register_commandstext command. - Go to Installation, choose a guild install context, and give the bot the
botandapplications.commandsscopes with the Send Messages and Embed Links permissions. - Open the generated install link and add the bot to your server.
- Go to Better Stack API tokens, select your team, and copy a token from the Uptime API tokens section or create one.
- Open your status page in the Better Stack dashboard. The URL ends in the status page ID, as in
https://uptime.betterstack.com/status-pages/123456. - Note the public address of the page, such as
https://status.example.com/. The bot readsfeed.rssfrom that address, which Better Stack publishes for every status page.
Copy the sample configuration and fill it in:
cp .env.example .env
To find a Discord channel, role, or user ID, turn on Developer Mode in Discord under Settings, then Advanced. Right-click the channel, role, or user and select Copy ID.
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_TOKEN |
Yes | Bot token from the Discord Developer Portal | |
API_TOKEN |
Yes | Better Stack Uptime API token | |
STATUS_PAGE_ID |
Yes | Numeric ID of your Better Stack status page | |
STATUS_PAGE_URL |
Yes | Public address of the status page, such as https://status.example.com/ |
|
UPDATES_CHANNEL_ID |
No | Channel that receives incident announcements. Without it, the bot serves /status only |
|
UPDATE_ROLE_ID |
No | Role to mention in each announcement. Without it, announcements mention nobody | |
BOT_OWNERS |
No | Comma separated user IDs that get owner commands. The Discord application owner and any team members always have them | |
COMMAND_PREFIX |
No | ~ |
Prefix for text commands |
POLL_INTERVAL_SECS |
No | 60 |
Seconds between RSS feed checks |
DATABASE_URL |
No | sqlite:status.db |
Location of the SQLite file that records posted entries |
RUST_LOG |
No | info |
Log level. Use debug while troubleshooting |
Keep .env out of version control. The included .gitignore already excludes it.
cargo run --release
The first run creates status.db, records the entries already on your feed, and stays quiet about them. Only entries published after that first run get announced.
Send ~register_commands in any channel the bot can read. The bot replies to confirm, and /status becomes available within a minute.
docker build -t betterstack-status-bot .
docker run -d --name status-bot --restart unless-stopped \
--env-file .env \
-e DATABASE_URL=sqlite:/data/status.db \
-v status-bot-data:/data \
betterstack-status-bot
The volume keeps the record of announced entries across restarts. Without it, the bot treats every entry on your feed as new after each recreate.
To keep the bot running on a Linux server, install the binary and add a systemd unit at /etc/systemd/system/betterstack-status-bot.service:
[Unit]
Description=Better Stack Status Bot
After=network-online.target
[Service]
Type=simple
User=statusbot
WorkingDirectory=/opt/betterstack-status-bot
EnvironmentFile=/opt/betterstack-status-bot/.env
ExecStart=/opt/betterstack-status-bot/betterstack-status-bot
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Then enable it:
sudo systemctl enable --now betterstack-status-bot
The bot polls STATUS_PAGE_URL plus feed.rss on the interval you set. Every entry gets an identifier built from its GUID and publication date, and the bot inserts that identifier into the guids table in SQLite with INSERT OR IGNORE. An insert that changes a row means the entry is new, so the bot announces it. An insert that changes nothing means the bot has seen the entry, so it stays quiet. Announcements go out oldest first, which keeps the channel in chronological order during an incident that produces several updates at once.
On a first run, when the table is still empty, the bot records everything already on the feed without announcing any of it. That keeps your channel from filling with your incident history the first time you start the bot. On every later start it announces anything published while it was offline.
The /status command takes a different path. It calls the Better Stack resources endpoint for your status page and builds the embed from the response, so the numbers are current at the moment someone asks rather than cached from the last poll. The command follows the pagination links in the response, and it splits the reply across several embeds because Discord allows only 25 fields in one.
git clone https://github.com/Aster-Privacy/Betterstack-status-bot.git
cd betterstack-status-bot
cargo build --release
The binary lands in target/release/. To check the code the way CI does, run:
cargo clippy --all-targets -- -D warnings
cargo +nightly fmt --check
Formatting uses nightly-only options from rustfmt.toml, which is why cargo fmt runs on the nightly toolchain. Building and linting work on stable.
| Symptom | Cause and fix |
|---|---|
| The bot starts and exits at once | A required variable is missing. The log names it. Check .env against the table above |
/status never appears in Discord |
Run ~register_commands as the owner of the Discord application, or add your user ID to BOT_OWNERS. Global commands take up to a minute to appear |
~register_commands gets no reply |
Turn on Message Content Intent in the Developer Portal, then restart the bot |
| Nothing gets announced | Confirm UPDATES_CHANNEL_ID is set and the bot can send messages and embed links in that channel. Set RUST_LOG=debug to see each poll |
| Old incidents get announced again | The SQLite file was lost. Point DATABASE_URL at persistent storage, or mount a volume when you use Docker |
Join our Discord to share feedback, ask questions, and contribute to the privacy community. You can also find us on X and Reddit.
If you have any questions or security disclosures, email us at hello@astermail.org or security@astermail.org. Do not open a public issue for security vulnerabilities. Read SECURITY.md for the full security vulnerability disclosure process.
Issues and pull requests are welcome. Keep changes focused, run cargo clippy --all-targets -- -D warnings and cargo +nightly fmt before you open a pull request, and describe what you changed and why.
By contributing, you agree to release your contribution into the public domain under the same terms as the rest of this repository.
This project is released into the public domain under the Unlicense. Copy it, change it, sell it, and do whatever you want with it. No attribution required.
Aster's other projects are licensed under AGPL v3. This one is not, so nothing here places any obligation on your own code.
