SimpWatch tracks !simp callouts from Twitch and /simp calls from Discord, then renders a public leaderboard with window filters.
- Public leaderboard page with windows:
24h,7d,30d,all - Prometheus metrics endpoints for web and Twitch worker
- Auto dark mode using system preference (
prefers-color-scheme) - Narc leaderboard (callout count by caller)
- Twitch command parsing:
!simp-> credits channel broadcaster@<bot_username> simp @username-> credits exact username- optional reason:
@<bot_username> simp @username reason <text>,!simp reason <text>,@<bot_username> simp @username because <text>, or!simp because <text> - bamder incidents:
!bamder,!bamder <text>, or!bamder reason <text>
Examples:
!simp@simplympics simp @riikarii!simp reason gifted 10 subs@simplympics simp @riikarii reason gifted 10 subs!simp because sent another dono@simplympics simp @riikarii because sent another dono!bamder!bamder bad bean!bamder reason was out of pocket- Discord slash-only command:
/simp target:<user> reason:<optional text>
- Django admin for identity linking and score moderation
- Django admin bulk merge action for combining duplicate people records
- Configurable cooldown lever (default disabled)
- Docker and Docker Compose
- Python 3.12+ (local dev optional; for running migrations/manage.py outside Docker)
uvpackage manager (install viacurl -LsSf https://astral.sh/uv/install.sh | sh)
- Copy env file:
cp .env.example .env- Fill in required vars in
.env:
DJANGO_SECRET_KEYTWITCH_BOT_USERNAMETWITCH_CLIENT_IDTWITCH_CLIENT_SECRETTWITCH_BOT_IDTWITCH_BOT_ACCESS_TOKENTWITCH_BOT_REFRESH_TOKENDISCORD_BOT_TOKENDISCORD_GUILD_ID(optional; if set, slash command syncs to one guild)
For reverse proxy / HTTPS deployments (important for admin login CSRF):
DJANGO_CSRF_TRUSTED_ORIGINS(comma-separated origins, e.g.https://simp.example.com)DJANGO_TRUST_X_FORWARDED_PROTO=True(if TLS terminates at proxy)DJANGO_USE_X_FORWARDED_HOST=True(if host header comes from proxy)
- (Optional) Install dev dependencies locally for linting/testing:
uv sync- Start services:
docker compose up --build -d- Create Django admin user:
docker compose exec web python manage.py createsuperuser- Open:
- Leaderboard:
http://<server>:8000/ - Admin:
http://<server>:8000/admin/
If you want to develop locally without Docker:
# Install project with dev dependencies (pytest, pytest-django, etc.)
uv syncCreate a .env file or export variables:
export DJANGO_SECRET_KEY='dev-secret-key'
export DATABASE_URL='postgresql://user:pass@localhost:5432/simpwatch'
export CACHE_URL='redis://localhost:6379/1'Or use SQLite + local memory cache for quick local testing:
export DATABASE_URL='sqlite:///db.sqlite3'
export CACHE_URL='locmem://'cd web
# Run migrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Start dev server (default: http://localhost:8000)
python manage.py runserver# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=web.simpwatch
# Run specific test module
uv run pytest web/simpwatch/tests/test_views.pyEdit pyproject.toml to add/update package versions, then sync:
uv syncThis updates uv.lock with the exact resolved versions. Always commit both files.
SIMP_DEFAULT_POINTS: default1SIMP_DEFAULT_COOLDOWN_SECONDS: default0(disabled)CACHE_URL: defaultredis://redis:6379/1LEADERBOARD_CACHE_TTL_SECONDS: default15for near-real-time freshnessDJANGO_CSRF_TRUSTED_ORIGINS: empty by defaultDJANGO_TRUST_X_FORWARDED_PROTO:Falseby defaultDJANGO_USE_X_FORWARDED_HOST:Falseby defaultTWITCH_METRICS_ENABLED: defaulttrueTWITCH_METRICS_PORT: default9090
Set cooldown later in admin via ScoringConfig without code changes.
SimpWatch exposes metrics in Prometheus text format:
- Web app:
GET /metricson the web service port (8000by default) - Twitch bot:
GET /metricsonTWITCH_METRICS_PORT(9090by default)
When deploying with Helm, both web and Twitch services can include Prometheus scrape annotations by enabling:
prometheus.scrape.enabled=true
Useful Helm values:
web.metrics.enabledweb.metrics.pathtwitchBot.metrics.enabledtwitchBot.metrics.porttwitchBot.metrics.service.enabledprometheus.scrape.interval
The bot can now manage tokens in the database via the web admin interface, similar to the broadcaster onboarding flow. This approach provides:
- Automatic token refresh – tokens are refreshed on bot startup
- Admin UI management – rotate/revoke tokens from Django admin
- Persistent storage – tokens survive pod restarts
- Encryption at rest – tokens are encrypted in the database
Setup:
- Create a Twitch account for the bot (recommended, separate from your main account).
- Create a Twitch application and note the
CLIENT_IDandCLIENT_SECRET. - Set web env vars:
TWITCH_CLIENT_ID=<twitch client id>TWITCH_CLIENT_SECRET=<twitch client secret>TWITCH_TOKEN_REDIRECT_URI=https://<your-domain>/oauth/twitch/bot/callbackTWITCH_GRANT_ENCRYPTION_KEY=<random 32-byte base64 string>
- Start the web app and log into the admin panel with a staff user
- Visit
/oauth/twitch/bot/startto initiate the bot token setup flow - You'll be redirected to Twitch to authorize the bot account
- After authorization, the bot token will be stored in the database automatically
- Restart the Twitch bot worker – it will fetch and use the token from the database:
docker compose up -d --build bot_twitch
docker compose logs -f bot_twitchExpected log: Twitch bot ready user=<name> channels=[...].
Token Expiration: When a token expires, the bot automatically refreshes it on startup using the refresh token. Refreshed tokens are automatically saved to the database, so no manual updates are needed.
If you prefer to manage tokens via env vars (not recommended for production), you can still use the original approach:
- Create a Twitch account for the bot.
- Create a Twitch application and note the
CLIENT_IDandCLIENT_SECRET. - Generate a refreshable user token pair for the bot account with chat scopes (
user:read:chat,user:write:chat,user:bot). - In
.envor secrets, set:TWITCH_BOT_USERNAME=<bot account username>TWITCH_CLIENT_ID=<twitch client id>TWITCH_CLIENT_SECRET=<twitch client secret>TWITCH_BOT_ID=<bot account user id>TWITCH_BOT_ACCESS_TOKEN=<access token>TWITCH_BOT_REFRESH_TOKEN=<refresh token>
- Restart the bot worker
The bot will fall back to env vars if no database grant is found. Like the database approach, tokens are automatically refreshed on expiration.
If the bot is not a moderator in a broadcaster's channel, that broadcaster must
authorize your app with channel:bot so EventSub replies can be sent.
- Configure web env vars:
TWITCH_TOKEN_REDIRECT_URI=https://<your-domain>/oauth/twitch/callbackTWITCH_BROADCASTER_TOKEN_SCOPES=channel:botTWITCH_GRANT_ENCRYPTION_KEY=<long-random-value>
- Ensure the channel login is added via Django admin (
simpwatch > Twitch channels).
- Only logins configured as monitored Twitch channels can store grants.
- Send broadcaster to:
https://<your-domain>/oauth/twitch/start
- Broadcaster completes Twitch consent.
- Verify stored grant in Django admin:
Admin -> Simpwatch -> Twitch broadcaster grants
Revocation endpoint (staff-only):
POST /oauth/twitch/revokewith form fieldusername=<channel_login>
When a grant is missing/inactive, the bot still records events but skips reply sending for that channel.
- Go to the Discord Developer Portal and create an application.
- Add a bot user under the application.
- Enable bot permissions needed for slash commands in your server.
- Invite the bot with scopes:
botapplications.commands
- Copy the bot token into
.env:DISCORD_BOT_TOKEN=<token>
- (Recommended) Set one test guild for faster command sync:
DISCORD_GUILD_ID=<your server id>
- Restart only the Discord worker:
docker compose up -d --build bot_discord
docker compose logs -f bot_discordExpected log after successful auth: Discord bot ready: ....
- Twitch:
!simpin a configured channel should credit that channel broadcaster.@<bot_username> simp @usernameshould credit the exact username target.
- Discord:
- Use
/simp target:<member>in the server where bot is installed.
- Use
Then confirm updates at:
http://<server>:8000/http://<server>:8000/api/leaderboard?window=all
When someone is auto-registered separately across Twitch/Discord, you can merge them:
- Open
Admin -> Simpwatch -> People. - Select 2+ rows that represent the same person.
- Choose action:
Merge selected people into the first selected. - Run action.
Behavior:
- The first selected row (lowest ID) is kept as the canonical
Person. - All selected source rows are merged into it.
- Related
Identity.person,SimpEvent.target_person, andScoreAdjustment.target_personare reassigned. - Source
Personrows are deleted after reassignment.
Twitch bot auth failed: token invalid/expired or missingoauth:prefix.Discord bot auth failed: invalid token inDISCORD_BOT_TOKEN.- Discord slash command not visible yet:
- if
DISCORD_GUILD_IDis unset, global command sync can take time. - set
DISCORD_GUILD_IDand restartbot_discordfor quick sync.
- if
This repo includes a workflow at .github/workflows/docker-images.yml that builds container images for:
webbot_twitchbot_discord
Behavior:
- On pull requests: builds images (no push).
- On pushes to
main: builds and pushes to GHCR. - On tags
v*: builds and pushes tag-based images.
Published image naming:
ghcr.io/<owner>/simpwatch-webghcr.io/<owner>/simpwatch-bot-twitchghcr.io/<owner>/simpwatch-bot-discord
Default tags include branch/tag/sha, plus latest for the default branch.
To use these in Docker Swarm, reference the GHCR image tags in your stack file instead of local build: blocks.
All Dockerfiles now use uv for fast, deterministic builds:
# Install uv
RUN pip install --no-cache-dir uv
# Copy project config and install dependencies
COPY pyproject.toml uv.lock* /app/
RUN uv sync --frozen --no-dev
COPY . /appKey points:
uv sync --frozenensures exact versions fromuv.lock--no-devskips dev dependencies in production buildsuvis ~2-3x faster than pip for resolving and installing
The uv.lock file (generated by uv sync) is committed to the repo
for reproducible builds across all environments.
Use docker-compose.swarm.yml for Swarm stacks.
Important Swarm note:
env_fileis not used bydocker stack deploy, so the stack file defines explicitenvironmentkeys.
Example:
export DJANGO_SECRET_KEY='replace-me'
export DJANGO_DEBUG='False'
export DJANGO_ALLOWED_HOSTS='example.com,localhost,127.0.0.1'
export POSTGRES_DB='simpwatch'
export POSTGRES_USER='simpwatch'
export POSTGRES_PASSWORD='replace-me'
export POSTGRES_HOST='db'
export POSTGRES_PORT='5432'
export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
export CACHE_URL='redis://redis:6379/1'
export LEADERBOARD_CACHE_TTL_SECONDS='15'
export SIMP_DEFAULT_POINTS='1'
export SIMP_DEFAULT_COOLDOWN_SECONDS='0'
export TWITCH_BOT_USERNAME='your-bot-name'
export TWITCH_CLIENT_ID='your-client-id'
export TWITCH_CLIENT_SECRET='your-client-secret'
export TWITCH_BOT_ID='your-bot-user-id'
export TWITCH_BOT_ACCESS_TOKEN='your-access-token'
export TWITCH_BOT_REFRESH_TOKEN='your-refresh-token'
export DISCORD_BOT_TOKEN='your-discord-token'
export DISCORD_GUILD_ID='your-guild-id'
export WEB_IMAGE='ghcr.io/<owner>/simpwatch-web:latest'
export BOT_TWITCH_IMAGE='ghcr.io/<owner>/simpwatch-bot-twitch:latest'
export BOT_DISCORD_IMAGE='ghcr.io/<owner>/simpwatch-bot-discord:latest'
export BUGSINK_DSN='example-dsn'docker stack deploy -c docker-compose.swarm.yml simpwatchdocker stack services simpwatch
docker service logs -f simpwatch_web
docker service logs -f simpwatch_bot_twitch
docker service logs -f simpwatch_bot_discord