A developer-API platform with live Trove game data, plus the website and backend behind Better Trove Tools.
Developers sign up in a browser portal, mint scoped, rate-limited API tokens, and call read-only data endpoints with them. Underneath is a reusable platform — accounts, tokens, rate limiting, usage metrics, email, and admin — with the Trove data surfaces layered on top.
Fully dockerized. MongoDB + Redis + PostgreSQL, all persisted to local folders.
| API | https://api.aallyn.net — /v1/* + /health |
| Developer portal | https://dev.aallyn.net — sign up, mint tokens, view usage |
| API reference | https://docs.aallyn.net — interactive docs · GET /openapi.json is always current · /llms.txt for AI tools |
| Showcase site | https://trove.aallyn.net — the Better Trove Tools website |
Rust (native/): axum + tokio · MongoDB 7 · Redis 7 · PostgreSQL (leaderboards, market,
codexes) · Argon2 + JWT auth · hashed API tokens · minijinja templates for the website.
- Accounts — signup with captcha + anti-spam + breach check, email verification, password reset, GDPR export and account deletion.
- Sessions — short-lived access tokens with rotating refresh tokens and instant global revocation ("log out everywhere").
- API tokens — scoped (Discord-style bitmask), IP-allowlisted, expiring, rotatable.
Self-validating
kiwi_<body>_<checksum>format, and leaked tokens are auto-revoked via GitHub secret scanning. - Rate limiting — Redis sliding-window with per-endpoint overrides and standard
X-RateLimit-*/Retry-Afterheaders. - Usage & site analytics — every authenticated request is recorded and aggregated. The website counts page views with its own small script (no cookies, bots and prefetches left out) and groups them into visits at query time: channels and sources, entry and exit pages, sections, devices, browsers, languages, time on page and bounce rate, compared with the previous period, plus a live visitor count. Mod creators see the same for their own mods.
- Admin — superuser-only panel for users, tokens, usage, and feature toggles.
- Shared plumbing for new endpoints: cursor pagination, idempotency keys, request-id correlation, and one consistent error envelope.
Live Trove game data — all GET, read-only, with real-UTC unix timestamps. Most categories
are public (callable with no token at a stricter per-IP limit); a matching scope unlocks
the full per-token limit. See the API reference for every endpoint.
| Category | What it covers |
|---|---|
rotations |
server time, daily/weekly buffs, merchant timers, biomes, chaos chest, hourly challenge, calendar |
feeds |
Trove news, Twitch/YouTube/Bilibili, Trovesaurus events |
stats · gems |
stat tables, player sigil images, and a stateless gem simulator / build optimizer (the game's own stat rolls, level-up odds and costs) |
leaderboards · activity |
hourly in-game boards (with cheat detection) and player/class activity estimates |
market |
in-game marketplace listings and price history |
store |
the in-game cash-shop catalog — products, prices, sales, deals, lootbox odds |
codexes |
decoded game catalogs — 18 collectible types, plus the relationships between them (what crafts what, what unlocks what), badge requirements and progression trees |
updates |
archived game-update files, browsable and diffable per patch |
mods · modpacks |
.tmod tooling plus a hub for sharing and bundling mods |
embed |
short-lived preview tokens for the embeddable 3D/VFX viewer other sites can iframe |
events |
a live SSE stream so clients stop polling on the hour |
ocr |
reads a character stat sheet from a screenshot (self-hosted, no LLM) |
btt · misc |
Better Trove Tools release feeds, time tools, server status, supporters |
The API container also serves the Better Trove Tools website out of site/ — the landing
page, user manual, command reference, and live pages for leaderboards, player/class activity,
codexes, the mods hub, modpacks, the marketplace, game updates, server status, and server time.
Browsing is public; developing and publishing mods is a Discord-login action.
Game data rebuilds itself after each patch. Everything under assets/gamedata/ that
comes from the game files (class stats and abilities, allies, mounts, gems, rings, PvP curves,
delve modifiers, companions, fish, mementos, badges, recipes, wings/boats/sails/auras/Mag Riders/flasks/tomes/fishing poles, costumes, bomb skins, styles, titles, NPCs, blocks and placeables, quests and adventures, worlds, subclass powers, daily bonuses, leaderboards, items, upgrade trees, mastery, class rewards, lootbox odds, PvP power-ups, chat commands, the Power Rank / Mastery sigil art, gear rarities, forge rules and banners) has a decoder in native/kiwi-server/src/decode/. After the archiver syncs a new live-us patch,
each decoder reruns and writes to GAMEDATA_DIR (./.gamedata, shared by the api, web and bot
containers), which readers prefer over the repo copy. A decoder also reruns when its
code_version in decode/registry.rs changes: bump it by hand when a decoder's output changes.
One that fails, decodes nothing, or shrinks its output by more than 20% keeps serving the
previous file and shows up in the dev panel's Game data tab with the error, where it can be
rerun.
Regional maintenance. The admin panel's Configuration → Regional access setting
(geo_maintenance_countries) takes a list of country codes; website visitors from those
countries get the maintenance page (assets/maintenance.html, its voxel art drawn
by scripts/build_maintenance_page.py) instead of any page. The web container resolves the
country from the visitor's IP with DB-IP's free database, downloaded monthly to ./.geoip;
nothing about the visitor is stored. The API, bot and BTT app are unaffected.
Its 3D model and particle-effect viewers are also embeddable: another site can iframe
trove.aallyn.net/embed/viewer to preview a mod it hosts, a mod on the hub, a dressed character, or
anything in the live game — a creature by its own prefab name, or a single blueprint filename, which
resolves into the whole assembled creature.
Framing is allowed per-origin (embed.allowed_origins), and a mod a partner uploads to preview is
held in Redis for minutes, never written to disk. Partner guide:
docs/embed.html; code in native/kiwi-server/src/embed/.
cp .env.example .env # then set SECRET_KEY, MONGO_*, REDIS_PASSWORD (+ captcha/SMTP)
docker compose up -d --build
curl http://127.0.0.1:15546/health # {"status":"ok"}Data lives under ./data (bind-mounted). There's no migration code: when a model's
indexes change, wipe the data dir and rebuild.
The api, web and bot containers run kiwi-server, kiwi-web and kiwi-bot from
trove-rust:latest, a slim Debian image with the binaries, assets/ and the few tools they
shell out to (git, a JRE for FFDec). builder runs kiwi-builder from trove-builder:latest,
the same image plus SteamCMD and the small Python TroveUI's release scripts need.
See .env.example for the full list.
| Variable | Required | Notes |
|---|---|---|
SECRET_KEY |
yes | JWT signing key — openssl rand -base64 48 |
MONGO_ROOT_PASSWORD / MONGO_APP_PASSWORD |
yes | Mongo root + least-privilege app user |
REDIS_PASSWORD |
yes | Redis auth |
CAPTCHA_SECRET / CAPTCHA_SITEKEY |
prod | Captcha is enforced only when both are set |
ADMIN_EMAIL / ADMIN_PASSWORD |
no | Bootstraps the master superuser on startup |
SMTP_HOST (+ SMTP_*, MAIL_FROM) |
prod | Mail relay; if unset, email is logged instead of sent |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
no | Enables "Sign in with GitHub" |
All services bind to 127.0.0.1; the proxy routes api.aallyn.net → :15546 (only /v1/*
/health),dev.aallyn.net→:25470,docs.aallyn.net→:25468, andtrove.aallyn.net→:15547(all paths). SetFORWARDED_ALLOW_IPSto the address the proxy connects from: kiwi-server then takes the client IP from the rightmostX-Forwarded-Forentry the proxy added, so rate limits key on the real visitor and a client-sent header is ignored. Never set it to*, which trusts the client's own leftmost entry. Bodies are capped at 8 MB, except the.tmodtools and bot cfg ingests (/v1/mods/*,/v1/leaderboards/insert,/v1/market/insert) at 20 MB, and the Sound Studio + Mod Workshop builds (/site/sound-studio/build,/site/mod-workshop/*, which upload a whole bank or a whole mod at once) at 32 MB, and the Blueprint Editor (/site/blueprint-editor/*, whose save carries a per-voxel edit list and whose.qbimport carries four voxel grids) at 64 MB, and the Unlock Debug patcher (/site/unlock-debug, which uploads a whole game executable so seven bytes of it can change) at 160 MB — allow at least as much at the proxy.
assets/ # data the binaries read: game data baseline, rigs, locales, codebooks
builder/ # the five TroveAPI Python files TroveUI's release scripts import
native/
├── kiwi-core/ # pure logic and file formats (no server types)
├── kiwi-pil/ # Pillow-identical image encoding and text
├── kiwi-zlib/ # classic zlib, byte for byte
├── kiwi-server/
│ ├── src/bin/ # kiwi-web, kiwi-bot, kiwi-builder (kiwi-server is main.rs)
│ ├── src/routes/ # route registry; feature areas live beside it in src/
│ ├── src/middleware/ # security headers, CSP, CORS, gzip, idempotency, request ids
│ └── src/starlette/ # the frozen route table 404/405/307 answers come from
└── port/ # the port's parity specs and fixture generators (history)
site/ # showcase website (templates + static)
portal/ # developer-portal SPA
docs/ # static docs (guide + Redoc reference + llms.txt)
- Add a module under
native/kiwi-server/src/with arouter()and merge it inrouter()(src/routes/mod.rs); the website's pages go insrc/web/. - Gate it with
RequireScope<BIT>(orPublic<BIT>for tokenless reads) and append a new scope tosrc/auth/scopes.rs(bits are permanent — never renumber or reuse). - Return
ApiErrorfor failures so every error keeps the one envelope. - Unmatched paths are answered from
src/starlette/routes.json; when a route is removed, remove it there too or its path answers 500 instead of 404.
Every error uses one envelope (branch on code, not message); each carries a request_id:
{ "error": { "code": "rate_limited", "message": "…", "details": null, "request_id": "req_…" } }Default limits: signup 5/h/IP · login 10/5min/IP · API 120/min/token · token creation 3/day.
Fan-out surfaces (codexes, the mods hub, updates file serving) meter in their own widened
buckets — see the *_rate_limit_multiplier settings in native/kiwi-server/src/config.rs.
JSON responses over 1 KB are gzipped when the client offers it; streams are excluded
by content type so SSE stays unbuffered (middleware/compression.rs).
cargo fmt --all --manifest-path native/Cargo.toml
cargo clippy --workspace --all-targets --manifest-path native/Cargo.toml -- -D warnings
cargo test --workspace --manifest-path native/Cargo.toml # store tests are #[ignore]d: they need Mongo/Redis/Postgres
cargo run --manifest-path native/Cargo.toml --bin kiwi-server # reads .env; KIWI_BIND defaults to 0.0.0.0:8000Run the binaries from the repo root so assets/ and site/ resolve. scripts/ holds standalone
helpers (static minify, font subsetting, i18n audit, the maintenance-page generator).
scripts/backup-mongo.sh (gzip mongodump) and scripts/restore-mongo.sh (--drop
restore). Schedule the backup via cron and keep an off-box copy.