Discord bot + FastAPI service for creating, validating, assigning, extending, and deleting software license keys, backed by Supabase.
- Discord slash commands for admins/staff
- REST API for your external tools/applications
- API key auth on API routes
- In-memory license file generation for Discord delivery (no local file persistence)
- Supabase-backed storage for license groups and license records
- Rate limiting, request logging, and basic HTTP hardening headers
.
|- app.py # Runs bot + API in separate processes
|- bot.py # Discord bot commands
|- api.py # FastAPI endpoints
|- database.py # Supabase operations
|- utils.py # Key generation/duration/status helpers
|- config.py # Environment/config loading + validation
|- schema.sql # Database schema
|- requirements.txt
`- logs/
- Python 3.9+
- Discord bot token
- Supabase project (URL + key)
pip install -r requirements.txtRun schema.sql in Supabase SQL Editor.
Create .env in project root:
DISCORD_BOT_TOKEN=your_discord_bot_token
DISCORD_GUILD_ID=your_discord_guild_id
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your_supabase_key
API_HOST=0.0.0.0
API_PORT=8000
API_SECRET_KEY=replace_with_long_random_secret
API_KEY_HEADER_NAME=X-API-Key
API_ALLOWED_ORIGINS=http://localhost:3000
API_ALLOWED_HOSTS=localhost,127.0.0.1
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=60
LOG_LEVEL=INFO
LOG_FILE=licensing_system.logImportant:
API_SECRET_KEYmust not bechange-this-secret-keyor startup will fail.API_ALLOWED_ORIGINSis comma-separated.API_ALLOWED_HOSTSis comma-separated.
python src/app.pypython src/bot.pypython src/api.pyAPI docs:
- Swagger:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
All commands are slash commands.
Command:
/create type:licensegroup name:Premium
What it does:
- Creates group
PREMIUMif it does not exist.
Command:
/create type:license group:Premium amount:10 duration:30d
What it does:
- Creates 10 licenses in
PREMIUM - Expiration: now + 30 days
- Sends
30d_licenses.txtas Discord attachment - File is generated in memory and sent immediately (not written to disk)
Supported duration formats:
7d,30d,90d1m,6m1y
Command:
/check key:OP-PREMIUM-TI-A8K9M2X5L7-C
What it returns:
- status (
valid/invalid) - active vs expired
- created/expires timestamps
- time remaining
- assignment (if present)
Command:
/list group:Premium
What it returns:
- up to 20 entries in embed
- active/expired counts
- total count
Command:
/extend key:OP-PREMIUM-TI-A8K9M2X5L7-C duration:30d
What it does:
- Adds duration to license expiration
Command:
/assign key:OP-PREMIUM-TI-A8K9M2X5L7-C user:john_doe
What it does:
- Sets
assigned_to = john_doe
Command:
/delete type:license key:OP-PREMIUM-TI-A8K9M2X5L7-C
Command:
/delete type:licensegroup group:Premium
Command:
/botinfo
Base URL:
http://localhost:8000
Authentication:
- Send API key in header on API calls:
X-API-Key: <your API_SECRET_KEY>
Notes:
/docs,/redoc,/openapi.jsonare public by default.- Main API routes require key.
Use this shell variable first:
API_KEY="replace_with_your_secret"
BASE_URL="http://localhost:8000"curl -X POST "$BASE_URL/create/license/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"group": "Premium",
"amount": 5,
"duration": "30d"
}'curl -X GET "$BASE_URL/check/license/?key=OP-PREMIUM-TI-A8K9M2X5L7-C" \
-H "X-API-Key: $API_KEY"curl -X POST "$BASE_URL/extend/license/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"key": "OP-PREMIUM-TI-A8K9M2X5L7-C",
"duration": "30d"
}'curl -X POST "$BASE_URL/assign/license/" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"key": "OP-PREMIUM-TI-A8K9M2X5L7-C",
"user": "john_doe"
}'curl -X GET "$BASE_URL/list/licenses/?group=Premium" \
-H "X-API-Key: $API_KEY"curl -X DELETE "$BASE_URL/delete/license/?key=OP-PREMIUM-TI-A8K9M2X5L7-C" \
-H "X-API-Key: $API_KEY"curl -X GET "$BASE_URL/statistics/" \
-H "X-API-Key: $API_KEY"Recommended pattern:
- Validate local input format quickly.
- Call
/check/license/server-side (never expose API secret in frontend/public client). - Fail closed on API/network errors.
- Cache short-term validation results (for example 30-120 seconds) if needed.
import os
import requests
BASE_URL = os.getenv("LICENSE_API_BASE", "http://localhost:8000")
API_KEY = os.getenv("LICENSE_API_KEY")
def check_license_or_raise(license_key: str) -> dict:
if not API_KEY:
raise RuntimeError("Missing LICENSE_API_KEY")
response = requests.get(
f"{BASE_URL}/check/license/",
params={"key": license_key},
headers={"X-API-Key": API_KEY},
timeout=5,
)
if response.status_code == 404:
raise ValueError("License not found")
if response.status_code == 401:
raise PermissionError("License API auth failed")
if response.status_code >= 500:
raise RuntimeError("License API server error")
response.raise_for_status()
data = response.json()
if data.get("status") != "valid" or data.get("is_expired", True):
raise ValueError("License is invalid or expired")
return dataconst BASE_URL = process.env.LICENSE_API_BASE || "http://localhost:8000";
const API_KEY = process.env.LICENSE_API_KEY;
async function assertLicenseValid(licenseKey) {
if (!API_KEY) throw new Error("Missing LICENSE_API_KEY");
const url = new URL("/check/license/", BASE_URL);
url.searchParams.set("key", licenseKey);
const res = await fetch(url, {
method: "GET",
headers: { "X-API-Key": API_KEY },
});
if (res.status === 404) throw new Error("License not found");
if (res.status === 401) throw new Error("Unauthorized to license API");
if (!res.ok) throw new Error(`License API error: ${res.status}`);
const data = await res.json();
if (data.status !== "valid" || data.is_expired) {
throw new Error("Invalid or expired license");
}
return data;
}- Keep
LICENSE_API_KEYon backend only - Add request timeout and retry policy
- Block access when API says invalid/expired
- Log failed checks with request IDs/user IDs
- Optionally store
assigned_toand compare against current user/device
- Rotate
API_SECRET_KEYregularly. - Restrict
API_ALLOWED_ORIGINSandAPI_ALLOWED_HOSTSin production. - Do not expose API key in browser/mobile clients.
- Keep Discord bot permissions minimal.
- Use HTTPS and reverse proxy (Nginx/Caddy/Cloudflare) in production.
- Confirm
X-API-Keyheader is present. - Confirm key equals
API_SECRET_KEYfrom server env. - Check that there are no leading/trailing spaces in
.env.
- Ensure all required env variables are set.
- Ensure
API_SECRET_KEYis not default placeholder.
- Verify bot has
applications.commandsscope. - If using global commands, allow time for propagation.
- For faster updates, set
DISCORD_GUILD_ID.
- Re-run
schema.sql. - Validate
SUPABASE_URLandSUPABASE_KEY. - Check logs in
logs/.
- Fork the repository on GitHub.
- Clone your fork:
git clone https://github.com/DraxonV1/LicensingBot.git
cd LicensingBotgit remote add upstream https://github.com/DraxonV1/LicensingBot.git
git fetch upstreamgit checkout -b feature/<short-description>pip install -r requirements.txt
py -3 -m py_compile src\\api.py src\\bot.py src\\config.py src\\utils.py src\\database.py src\\app.py src\\terminal_logger.pygit add .
git commit -m "feat: <your change summary>"
git push origin feature/<short-description>- Open PR from your fork branch to
upstream/main. - Include:
- what changed
- why it changed
- how you tested
- any breaking changes
- Keep changes focused and small where possible.
- Update README and examples when behavior changes.
- Preserve existing API shape unless intentionally versioning.
- Add defensive error handling for external calls.
MIT (or project-defined license in repository settings).

