A GraphQL API serving BattleTech unit, equipment, faction, and era data sourced from MegaMek and the Master Unit List (MUL).
- API: Rust · axum 0.8 · async-graphql 7 · sqlx 0.8 · PostgreSQL 16
- Scraper: imports from MegaMek unit files (MTF + BLK formats) including per-slot critical hit tables, the Master Unit List (BV, roles, availability, clan names), equipment stats seed data (including ammo shots-per-ton), and ammo-to-weapon linkage
- Ops: Prometheus metrics at
/metrics, Dockerfile (musl/Alpine), IP rate limiting
1. Start Postgres
docker run -d --name bt-postgres \
-e POSTGRES_PASSWORD=pass \
-p 5432:5432 \
postgres:162. Configure environment
cp .env.example .env
# Edit DATABASE_URL if needed3. Install tooling and run migrations
cargo install sqlx-cli --no-default-features --features postgres
sqlx migrate run4. Seed the database
Option A — Use the included seed dump (fastest):
./seed/load.shThis loads a pre-exported snapshot with all MegaMek + MUL data in seconds.
Option B — Import from source:
Download a MegaMek release tarball (e.g. MegaMek-0.50.11.tar.gz) and extract it. The unit data is at data/mekfiles/unit_files.zip inside the extracted directory.
# Step 1: Import MegaMek unit files
cargo run -p scraper@0.1.0 --release -- megamek \
--zip /path/to/MegaMek-0.50.11/data/mekfiles/unit_files.zip \
--version "0.50.11"
# Step 2 (optional): Enrich with MUL data (BV, cost, role, availability, clan names)
# First fetch MUL data to local files:
cargo run -p scraper@0.1.0 --release -- mul-fetch \
--output-dir ./mul-data --delay-ms 1000
# Then import into DB:
cargo run -p scraper@0.1.0 --release -- mul-import \
--data-dir ./mul-data5. Run the API
cargo run -p apiThe server starts on http://localhost:8080. In debug builds, GraphiQL is available at GET /graphql.
| Endpoint | Description |
|---|---|
POST /graphql |
GraphQL API |
GET /graphql |
GraphiQL playground (debug builds only) |
GET /health |
Liveness check — always 200 |
GET /ready |
Readiness check — verifies DB connectivity and schema version |
GET /metrics |
Prometheus metrics |
GET /schema.graphql |
Full GraphQL schema in SDL format |
GET /llms.txt |
LLM-optimized API reference document |
# Paginated unit search with filters
{
units(first: 20, nameSearch: "Atlas", techBase: "inner_sphere") {
edges {
node {
slug
fullName
clanName
tonnage
bv
cost
introYear
rulesLevel
role
}
}
pageInfo {
totalCount
hasNextPage
endCursor
}
}
}
# Single unit with full detail and resolved component types
{
unit(slug: "atlas-as7-d") {
fullName
clanName
tonnage
techBase
bv
cost
role
mechData {
config
isOmnimech
engineRating
walkMp
runMp
jumpMp
heatSinkCount
# Resolved component types with construction properties
engine { name weightMultiplier ctCrits stCrits }
armor { name pointsPerTon crits }
structure { name weightFraction crits }
heatsink { name dissipation crits weight }
gyro { name weightMultiplier crits }
cockpit { name weight crits }
myomer { name }
# Raw MegaMek strings (always available as fallback)
engineTypeRaw
armorTypeRaw
}
loadout {
equipmentName
location
quantity
isRearFacing
}
locations {
location
armorPoints
rearArmor
structurePoints
}
quirks {
name
isPositive
description
}
availability {
factionSlug
factionName
eraSlug
eraName
availabilityCode
}
}
}
# Search Clan units by alternate name
# nameSearch matches both fullName and clanName
{
units(first: 5, nameSearch: "Fire Moth") {
edges {
node { slug fullName clanName }
}
}
}
# Filter by faction, era, and role
{
units(first: 20, factionSlug: "clan-wolf", eraSlug: "clan-invasion", role: "Striker") {
edges {
node {
slug
fullName
tonnage
bv
role
}
}
pageInfo { totalCount }
}
}
# All Clan factions
{
allFactions(isClan: true) {
slug
name
shortName
}
}
# Chassis with variants filtered by rules level
{
chassis(slug: "atlas-mech") {
name
unitType
tonnage
variants(rulesLevel: STANDARD) {
slug
fullName
rulesLevel
bv
introYear
}
}
}
# List chassis filtered by rules level (cumulative)
{
allChassis(unitType: MECH, rulesLevel: STANDARD) {
slug
name
techBase
tonnage
}
}
# Equipment with stats and ammo relationships
{
equipment(slug: "autocannon-10") {
name
tonnage
crits
damage
heat
rangeShort
rangeMedium
rangeLong
bv
observedLocations
ammoTypes { slug name }
}
}
# Equipment search with builder filters
{
allEquipment(maxTonnage: 2.0, maxCrits: 3, observedLocation: "right_arm") {
edges {
node { slug name tonnage crits }
}
}
}
# Construction reference — all component types in one request
{
constructionReference {
engineTypes { slug name techBase weightMultiplier ctCrits stCrits }
armorTypes { slug name pointsPerTon crits }
structureTypes { slug name weightFraction crits }
heatsinkTypes { slug name dissipation crits weight }
gyroTypes { slug name weightMultiplier crits }
cockpitTypes { slug name weight crits }
myomerTypes { slug name }
engineWeights { rating standardWeight }
internalStructure { tonnage head centerTorso sideTorso arm leg }
}
}The units query supports the following filters:
| Filter | Type | Description |
|---|---|---|
nameSearch |
String | Case-insensitive substring match on fullName and clanName |
techBase |
String | inner_sphere, clan, mixed, primitive |
rulesLevel |
Enum | Cumulative rules level filter. E.g. ADVANCED includes introductory, standard, and advanced |
tonnageMin / tonnageMax |
Float | Weight range in metric tons |
factionSlug |
String | Units available to this faction (e.g. "clan-wolf") |
eraSlug |
String | Units available in this era (e.g. "clan-invasion") |
isOmnimech |
Bool | OmniMechs only (true) or non-OmniMechs (false) |
config |
String | Chassis config: Biped, Quad, Tripod, LAM |
engineType |
String | Engine type (e.g. "XL Engine", "Fusion Engine") |
hasJump |
Bool | Jump-capable mechs only |
role |
String | Tactical role (e.g. "Juggernaut", "Sniper", "Striker") |
The allEquipment query supports additional builder-oriented filters:
| Filter | Type | Description |
|---|---|---|
nameSearch |
String | Case-insensitive substring match on equipment name |
category |
String | Equipment category in snake_case (e.g. "energy_weapon") |
techBase |
String | inner_sphere, clan, mixed, primitive |
rulesLevel |
Enum | Cumulative rules level filter. E.g. ADVANCED includes introductory, standard, and advanced |
maxTonnage |
Float | Equipment weighing at most this many tons |
maxCrits |
Int | Equipment consuming at most this many critical slots |
observedLocation |
String | Equipment observed at this location (e.g. "right_arm") |
ammoForSlug |
ID | Ammo types compatible with this weapon slug |
- Query depth: 20
- Query complexity: 500
unitsByIds: max 24 slugs per call- Pagination: max 100 per page
- Rate limit: 100 req burst / ~120 req/min sustained per IP
docker build -t battletech-api .
docker run -p 8080:8080 \
-e DATABASE_URL=postgres://postgres:pass@host.docker.internal:5432/battletech \
-e ALLOWED_ORIGINS=https://yourdomain.com \
-e EXPECTED_SCHEMA_VERSION=1 \
battletech-apiThe image is a statically-linked musl binary on Alpine (~10 MB).
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
PORT |
8080 |
Listen port |
ALLOWED_ORIGINS |
— | Comma-separated CORS origins; use * to allow all |
EXPECTED_SCHEMA_VERSION |
1 |
Schema version checked by /ready |
RUST_LOG |
info |
Log filter (e.g. debug, warn, api=debug) |
PUBLIC_BASE_URL |
http://localhost:{PORT} |
Base URL used in /llms.txt and /schema.graphql references |
Infrastructure is managed with Terraform in the infra/ directory. It provisions a managed Kubernetes cluster and a managed PostgreSQL instance on timeweb.cloud, connected via a private VPC.
1. Configure variables
cd infra
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars — set twc_token, db_password, and other values2. Provision infrastructure
terraform init
terraform plan
terraform apply3. Upload static assets to S3
The S3 bucket (resources.battledroids.ru) hosts static assets such as record sheet images. After terraform apply creates the bucket:
cd infra
./scripts/upload-assets.sh ../../battletech-roster-builder/packages/record-sheet/assetsThis syncs template and pattern images under roster/templates/ and roster/patterns/, and configures CORS for roster.battledroids.ru.
4. Get kubeconfig and apply K8s manifests
# Save kubeconfig from Terraform output
terraform output -raw kubeconfig > ~/.kube/battletech.yaml
export KUBECONFIG=~/.kube/battletech.yaml
# Create namespace and deploy the API
kubectl apply -f k8s/namespace.yaml
kubectl create secret generic battletech-api-secrets \
--namespace=battletech \
--from-literal=DATABASE_URL='postgres://USER:PASS@DB_HOST:5432/battletech'
kubectl apply -f k8s/api.yaml5. Run migrations and seed the database
Connect to the managed PostgreSQL using the VPC-internal address (from terraform output db_host), then:
DATABASE_URL='postgres://USER:PASS@DB_HOST:5432/battletech' sqlx migrate run
DATABASE_URL='postgres://USER:PASS@DB_HOST:5432/battletech' ./seed/load.shUnits, chassis, equipment, locations, loadout, quirks, and mech-specific data are imported from MegaMek release files. The scraper reads .mtf (mech) and .blk (vehicle, aerospace, etc.) formats from MegaMek's unit_files.zip.
The scraper enriches MegaMek data with information from the official Master Unit List:
- Battle Value (BV) and C-bill cost for game balancing
- Tactical roles (Juggernaut, Sniper, Striker, Brawler, etc.)
- MUL ID linking to the official entry
- Clan names — alternate IS/Clan reporting names for dual-name OmniMechs (e.g. "Fire Moth" for "Dasher")
- Faction/era availability — which factions field each unit in which eras
MUL data is fetched via mul-fetch (saves to local files, resume-safe) and imported via mul-import. A pre-fetched archive is included at mul-data.zip. Units are matched by slug (~95% match rate for BattleMechs/vehicles).
All imports are idempotent — inserts use ON CONFLICT ... DO UPDATE.
| Table | Rows | Source |
|---|---|---|
unit_chassis |
~1,670 | MegaMek |
units |
~6,535 | MegaMek |
unit_mech_data |
~4,225 | MegaMek |
equipment |
~2,875 | MegaMek |
unit_loadout |
~70,550 | MegaMek |
unit_locations |
~33,150 | MegaMek |
unit_availability |
~100,000+ | MUL |
eras |
10 | seed + MUL |
factions |
~70 | seed + MUL |
engine_types |
9 | construction ref |
armor_types |
9 | construction ref |
structure_types |
6 | construction ref |
heatsink_types |
4 | construction ref |
gyro_types |
5 | construction ref |
cockpit_types |
6 | construction ref |
myomer_types |
4 | construction ref |
engine_weight_table |
79 | construction ref |
mech_internal_structure |
17 | construction ref |