- Rust 98.1%
- Shell 1.5%
- PLpgSQL 0.2%
- Dockerfile 0.2%
| .forgejo/workflows | ||
| dev | ||
| migrations | ||
| scripts | ||
| selfhost | ||
| src | ||
| tests/it | ||
| .gitignore | ||
| .releaserc.cjs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| LICENSE | ||
| openapi.json | ||
| README.md | ||
| renovate.json | ||
| rust-toolchain.toml | ||
| SELFHOSTING.md | ||
skinhub-api
Rust backend (axum) for skins.sulej.net. The UI lives in skinhub/skinhub-ui.
Stack
- Rust 2024, axum 0.8 (tokio / tower)
- sea-orm 1.1 + sqlx (Postgres)
- reqwest (osu! API)
- aws-sdk-s3 (Garage S3 for skin source, rendered media, staging)
- utoipa + utoipa-axum (OpenAPI generated from the code)
Local dev
dev/dev.sh runs the whole platform locally: dockerized Postgres + MinIO,
this api under cargo run (with DEV_LOGIN=1 so /api/auth/dev-login mints
sessions without osu! OAuth), the sibling ../skinhub-ui vite dev server, and
seeded multi-persona test data (dev/seed.sh prints login links for a platform
admin, a community admin, members, and outsiders). dev/dev.sh down stops it.
Bare-api alternative:
# DATABASE_URL is the only required env (SESSION_SECRET is generated into the
# DB when unset); see src/config.rs for the full list.
export DATABASE_URL='postgres://skinhub:skinhub@localhost:5432/skinhub'
cargo run # serves on 0.0.0.0:8000
cargo run -- --dump-openapi # print the OpenAPI spec and exit (no DB needed)
Settings written by the first-run setup page and /admin/settings live in the
app_settings table and override env on boot (src/services/app_settings.rs);
applying a change restarts the process.
Self-hosting: see SELFHOSTING.md (short version: possible, not recommended).
Schema: on boot the binary applies its migrations idempotently to whatever database it connects to — the 0001 bootstrap DDL plus the ledgered migrations/000X_*.sql, each recorded once in _skinhub_migrations.
Tests
scripts/test.sh # full suite, safely (recommended)
scripts/test.sh --coverage # + merged llvm-cov report (target/llvm-cov/html)
scripts/test.sh upload # filtered subset (cargo test name filter)
cargo test --lib # unit tests only — no containers, instant
All integration tests live in one binary (tests/it) and run against a single shared Postgres that scripts/test.sh provisions and tears down (or a running one named by TEST_DATABASE_URL); each test gets its own database (cloned from a template), its own in-process S3 fake and in-process server, with the osu! API faked at the HTTP boundary (wiremock). Without TEST_DATABASE_URL the integration tests refuse to run, so a bare cargo test cannot leak anything. cargo clippy -- -D warnings and cargo fmt --check are the lint gates.
Deployment
CI on push to main/beta builds the image, snapshots and validates the OpenAPI spec, commits openapi.json back to the branch, and pushes git.sulej.net/skinhub/skinhub-api (:beta on the beta branch). A releasable push bumps the pinned tag in skinhub/deploy and Komodo redeploys the stack.
Traefik routes skins.sulej.net/api/* → skinhub-api:8000.
OpenAPI
The spec is generated from the code (utoipa) and served live at /api/openapi.json, rendered at /api/docs (Scalar). CI commits the source-derived openapi.json to the branch; skinhub-ui runs pnpm run sync-types against that committed, branch-matched spec to regenerate its TypeScript types.
Permissions
The site is locked to authenticated users — there is no anonymous browsing. Anonymous requests reach only a small public carve-out (/api/auth/*, health, docs, openapi, announcements, /api/og/*) and backend-minted signed media URLs.
| Action | Member (session) | Owner | Admin |
|---|---|---|---|
| Browse / read skins, roster | ✓ | ✓ | ✓ |
| Edit own collection | ✓ | ✓ | |
| Edit any collection | ✓ |
License
Licensed under AGPL-3.0-only. The LICENSE file was added in 2026; all prior versions, releases, and container images (which predate the LICENSE file in the source tree) are also offered under AGPL-3.0 at the discretion of the copyright holder.