Discord K-Pop photocard collection bot powered by FastAPI and PostgreSQL.
Bernice is a Discord Interactions bot for collecting gacha-style K-Pop idol photocards. A FastAPI webhook receives Discord interaction payloads, verifies Discord signatures, performs card and inventory work in PostgreSQL, then edits the deferred Discord response through the Discord webhook API.
About | Features | Built With | Getting Started | Usage | Deployment
Bernice gives a Discord server a lightweight photocard collection loop: users roll cards, build an inventory, page through their collection, and view individual cards by public code. The app is webhook-first, so it can run as a normal HTTP service without maintaining a long-lived Discord gateway connection.
The project includes a local Docker Compose setup, PostgreSQL schema and seed data, slash command registration, and a Render-oriented healthcheck workflow.
- Discord slash commands handled through Interaction webhooks.
/droprolls a random photocard, increments the global print count, and adds the card to the user's inventory./inventoryshows a paginated inventory with Discord button components for previous/next navigation./view code:<public_code>displays card metadata, image, global print count, and the requester's copy count./statusreturns a creator-only status embed with a button-driven status display.- PostgreSQL schema and seed data for users, artists, idols, card sets, cards, inventories, and rarity rates.
/healthendpoint for uptime checks and Render warmup pings.
| Layer | Tools |
|---|---|
| App runtime | Python, FastAPI, Uvicorn |
| Database | PostgreSQL, asyncpg |
| Discord integration | Discord Interactions webhooks, httpx, requests |
| Local development | Docker Compose |
| Automation | GitHub Actions, UptimeRobot |
Discord slash command
|
v
POST /interactions
|
v
Discord signature verification
|
v
FastAPI router dispatches command/component
|
v
Background handler calls app service
|
v
PostgresRepository performs database work
|
v
Discord webhook PATCH edits original deferred response
The FastAPI lifespan hook in src/server/main.py creates a shared PostgresRepository, initializes an asyncpg connection pool, loads rarity rates at startup, and closes the pool on shutdown.
Note
Bernice retries database startup up to 5 times. Discord webhook PATCH requests currently log rate-limit responses, but they do not retry failed or rate-limited Discord responses.
| Path | Purpose |
|---|---|
src/server/ |
FastAPI app, routers, Discord signature verification, and Discord response handlers |
src/app/ |
Domain models and service functions for drop, inventory, and card view workflows |
src/infra/ |
PostgreSQL repository and Discord webhook client adapters |
init/ |
Database schema and seed data used by the Postgres container |
tests/ |
Current manual/unit test files |
register_commands.py |
Registers Discord slash commands to the development guild |
.github/workflows/ |
Scheduled Render healthcheck workflow |
- Python 3.10+ recommended. The Docker image uses Python 3.12, and
setup.pyallows Python 3.8+. - Docker and Docker Compose for the easiest local database setup.
- A Discord application with a bot token and public key.
- An HTTPS tunnel such as ngrok for local Discord interaction testing.
From the repository root:
py -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
pip install -e .pip install -e . makes the server, app, and infra packages importable from the src/ layout.
docker compose up --buildThis starts:
| Service | URL/Port | Notes |
|---|---|---|
app |
http://localhost:8000 |
FastAPI app with /docs, /health, and /interactions |
db |
localhost:5433 |
Postgres container mapped to internal port 5432 |
Useful local endpoints:
| Endpoint | Purpose |
|---|---|
GET /docs |
FastAPI OpenAPI documentation |
GET /health |
Healthcheck for local checks and Render warmup |
HEAD /health |
Lightweight healthcheck variant |
POST /interactions |
Discord Interactions webhook endpoint |
Use this when you already have a configured PostgreSQL database and DATABASE_URL points to it:
python -m uvicorn server.main:app --reload --port 8000Start an HTTPS tunnel to the local FastAPI app:
ngrok http 8000In the Discord Developer Portal, set the Interactions Endpoint URL to:
https://YOUR-TUNNEL-HOST/interactions
Then register slash commands:
python register_commands.pyregister_commands.py currently registers guild-scoped commands for faster development updates.
Create a .env file at the repository root.
| Variable | Used by | Description |
|---|---|---|
DATABASE_URL |
App, repository, tests | PostgreSQL connection string read by PostgresRepository |
DATABASE_URL_DOCKER |
Docker Compose | Container-facing database URL assigned to DATABASE_URL inside the app container |
POSTGRES_USER |
Postgres container | Local database username |
POSTGRES_PASSWORD |
Postgres container | Local database password |
POSTGRES_DB |
Postgres container | Local database name |
DISCORD_APPLICATION_ID |
App and command registration | Discord application id |
DISCORD_TOKEN |
register_commands.py |
Bot token used to register slash commands |
DISCORD_PUBLIC_KEY |
App | Public key used to verify Discord interaction signatures |
Example local shape:
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/bernice
DATABASE_URL_DOCKER=postgresql://postgres:postgres@db:5432/bernice
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=bernice
DISCORD_APPLICATION_ID=your_application_id
DISCORD_TOKEN=your_bot_token
DISCORD_PUBLIC_KEY=your_public_keyAfter the app is running and the Interactions Endpoint URL points to /interactions, users can interact with Bernice through Discord slash commands.
| Command | Behavior |
|---|---|
/drop |
Rolls a random card, creates the user if needed, increments print count, and adds/increments inventory quantity |
/inventory |
Shows up to 10 distinct card lines per page with previous/next buttons |
/view code:<public_code> |
Shows a specific card by public code, including image and copy counts |
/status |
Creator-only status embed with a status button |
init/1_init_tables.sql creates the schema, and init/2_init_data.sql seeds:
- Rarity rates for rarities 1 through 5.
- Artists including NewJeans, LE SSERAFIM, ITZY, aespa, and BIGBANG.
- 23 idols.
- A
Regularcard set. - 50 seeded card catalog rows with Cloudinary image URLs.
Note
The repository currently falls back to rarities 1 through 3 when a sampled rarity is above 3, because the catalog is not fully populated for higher rarities yet.
There is no configured CI test runner for the project yet, but the repository includes two test files:
python tests\test_postgres_repository.py
python -m unittest tests\test_discord_client.pyCurrent caveats:
tests/test_postgres_repository.pyis a manual async smoke test and needs a realDATABASE_URL.tests/test_discord_client.pyappears stale relative tosrc/infra/discord/client.py; it imports retry helpers that are not present in the current implementation.
The Dockerfile installs dependencies, installs the package in editable mode, exposes port 8000, and runs:
python -m uvicorn server.main:app --host 0.0.0.0 --port 8000For Render-style deployment, use a port provided by the platform:
uvicorn server.main:app --host 0.0.0.0 --port $PORTThe workflow in .github/workflows/keep-render-warm.yml can ping a deployed healthcheck URL every five minutes when the RENDER_HEALTHCHECK_URL repository secret is configured.
Install the package from the repository root:
pip install -e .Then run Uvicorn with the package import path:
python -m uvicorn server.main:app --reload --port 8000PostgresRepository reads DATABASE_URL when the module imports. Add DATABASE_URL to .env or export it in the shell before starting the app.
Confirm that:
- The URL is HTTPS and ends in
/interactions. DISCORD_PUBLIC_KEYis set correctly.- The local app is reachable through the tunnel.
- The app can connect to PostgreSQL during startup.