A campaign-aware Dungeon Master's assistant. The DM maintains a campaign's state (party level, location, tone, NPCs, background lore, player characters) and uses it to generate context-grounded content — encounters, loot, NPCs, descriptions — so the AI stays consistent with your established story instead of being context-blind like a static random table. Generations are saved to a session log.
| Feature | Description |
|---|---|
| Multi-campaign | Create and switch between campaigns from the header |
| AI generation | encounter / loot / npc / description — all campaign-aware |
| World Bible | Background story, key characters, player info — injected into every prompt as immutable lore |
| Session log | All generations + DM journal notes, newest-first |
| Recent log context | Last 8 log entries (≤ 3 000 chars) fed into each generation prompt |
| Export / Import | Back up or migrate campaigns as JSON, CSV, or TSV |
| API flexibility | OpenRouter · OpenAI · Anthropic · Gemini · Ollama · Custom — switch in-app, no restart |
| In-app API key | Key stored in the local DB; no config file required |
- Backend: FastAPI + uvicorn (Python 3.12)
- Database: SQLite (stdlib
sqlite3), file atdata/gm_assist.db, schema indata/schema.sql - Frontend: Vanilla JS + Tailwind CSS via CDN — no build step
- AI: Configurable (default: OpenRouter →
openai/gpt-4o-mini)
git clone <repo>
cd B1_DM_assist
pip install -r requirements.txt
# optional: set a fallback key in .env (can also be set in the UI)
cp .env.example .env
uvicorn src.main:app --reload --port 8002
# → open http://localhost:8002Open the Settings tab to choose your API provider and paste your key. The key is saved to data/gm_assist.db.
SQLite file: data/gm_assist.db — created automatically on first run. Schema source: data/schema.sql.
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | auto-increment |
name |
TEXT NOT NULL | |
system |
TEXT | e.g. "D&D 5e", "Pathfinder 2e" |
setting |
TEXT | one-line world description |
tone |
TEXT | e.g. "dark & gritty", "whimsical" |
party_level |
INTEGER | default 1 |
current_location |
TEXT | where the party is now |
recent_events |
TEXT | rolling DM notes on what just happened |
known_npcs |
TEXT | operational NPC list (names, roles, secrets) |
background_story |
TEXT | World Bible — core lore, history, prophecies |
key_characters |
TEXT | World Bible — major NPCs in depth |
player_info |
TEXT | World Bible — player characters, backstories |
created_at |
TEXT | datetime('now') UTC |
Operational state (left panel) changes session-to-session. World Bible (centre tab) is the immutable foundation — always injected into AI prompts.
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK | |
campaign_id |
INTEGER | FK → campaigns.id |
created_at |
TEXT | UTC |
kind |
TEXT | encounter · loot · npc · description · note |
source |
TEXT | ai (generated) or dm (journal note) |
dm_input |
TEXT | what the DM described |
title |
TEXT | short punchy title |
content |
TEXT | main read-aloud text |
mechanic |
TEXT | dice / DCs / stats (empty string if none) |
secret |
TEXT | DM-only twist |
connects_to |
TEXT | tie-in to NPC / event |
is_favorite |
INTEGER | 0/1, default 0 |
Global key/value store. One row per setting.
| Key | Default | Description |
|---|---|---|
api_provider |
openrouter |
Active provider |
api_model |
openai/gpt-4o-mini |
Model name / ID |
api_key |
"" |
API key (plaintext, local only) |
api_base_url |
"" |
Override base URL (Ollama / Custom) |
Environment variables are fallbacks — the in-app Settings tab takes precedence.
| Variable | Used when | Description |
|---|---|---|
OPENROUTER_API_KEY |
provider = openrouter |
OpenRouter API key |
OPENAI_API_KEY |
provider = openai |
OpenAI API key |
ANTHROPIC_API_KEY |
provider = anthropic |
Anthropic API key |
GEMINI_API_KEY |
provider = gemini |
Google Gemini API key |
MODEL |
no DB model set | Fallback model name |
API_PROVIDER |
no DB provider set | Fallback provider |
| Provider | Base URL | Notes |
|---|---|---|
openrouter |
https://openrouter.ai/api/v1 |
Recommended — one key, all models |
openai |
https://api.openai.com/v1 |
|
anthropic |
https://api.anthropic.com/v1 |
Uses native Messages API format |
gemini |
https://generativelanguage.googleapis.com/v1beta/openai |
OpenAI-compat endpoint |
ollama |
http://localhost:11434/v1 |
Self-hosted, no key needed |
custom |
user-defined | Any OpenAI-compatible endpoint |
pip install -r requirements.txt
uvicorn src.main:app --host 0.0.0.0 --port 8002docker compose up --build
# → http://localhost:8002The data/ directory is bind-mounted so the database survives container restarts. Set your API key via the in-app Settings tab.
# docker-compose.yml — adjust volume path for your NAS
services:
dm-assist:
build: .
ports:
- "8002:8002"
volumes:
- /volume1/docker/dm-assist/data:/app/data # Synology example
restart: unless-stoppeddocker compose up -d --buildAccess at http://<nas-ip>:8002. For external access, forward the port in your router — or use a VPN for better security.
git clone <repo> && cd B1_DM_assist
docker compose up -d --buildnginx reverse proxy:
server {
listen 80;
server_name dm.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8002;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Add TLS: certbot --nginx -d dm.yourdomain.com
Add basic auth to restrict access:
htpasswd -c /etc/nginx/.htpasswd dmuser
# nginx server block: auth_basic "DM Assist"; auth_basic_user_file /etc/nginx/.htpasswd;Railway — railway init && railway up. Add a Volume at /app/data.
Render — Web Service. Build: pip install -r requirements.txt. Start: uvicorn src.main:app --host 0.0.0.0 --port $PORT. Add a Persistent Disk at /app/data.
Fly.io
fly launch
fly volumes create dm_data --size 1
# fly.toml: [[mounts]] source="dm_data" destination="/app/data"
fly deployNote: Cloud deployments are public by default. Add HTTP basic auth or a FastAPI auth middleware before sharing the URL.
pytest -q # run tests (no network — _call_model is monkeypatched)
uvicorn src.main:app --reload --port 8002 # dev server