Rust backend for skins.sulej.net (backend for skinhub/skinhub-ui)
  • Rust 98.1%
  • Shell 1.5%
  • PLpgSQL 0.2%
  • Dockerfile 0.2%
Find a file
2026-08-18 22:25:31 +00:00
.forgejo/workflows feat!: drop the release changelog feed; announcements are the only what's-new surface 2026-08-18 21:34:11 +02:00
dev refactor!: drop the git-backend skin importer and legacy forgejo bucket; rename garage_* config to s3_* 2026-08-16 18:38:06 +02:00
migrations feat: operational discord alerts, abuse signals and a daily digest 2026-08-18 18:12:18 +02:00
scripts feat!: drop the release changelog feed; announcements are the only what's-new surface 2026-08-18 21:34:11 +02:00
selfhost docs: point the self-hosting guide at the stable :latest images 2026-08-16 15:45:29 +02:00
src fix: raise the per-IP announcements limit to 300/min 2026-08-19 00:20:52 +02:00
tests/it fix: raise member-facing rate limits, hide admin-only render dispatch from the catalog 2026-08-19 00:20:52 +02:00
.gitignore feat: first-run setup, web-editable settings, dev-login, selfhost and dev stacks 2026-08-15 17:12:14 +02:00
.releaserc.cjs ci: keep Cargo.lock in step with the released version and build --locked 2026-08-17 05:56:20 +02:00
Cargo.lock chore(release): 10.1.0 [skip ci] 2026-08-18 22:23:39 +00:00
Cargo.toml chore(release): 10.1.0 [skip ci] 2026-08-18 22:23:39 +00:00
CHANGELOG.md chore(release): 10.1.0 [skip ci] 2026-08-18 22:23:39 +00:00
Dockerfile feat!: drop the release changelog feed; announcements are the only what's-new surface 2026-08-18 21:34:11 +02:00
LICENSE chore: add AGPL-3.0 license 2026-05-19 11:57:06 +02:00
openapi.json chore: update openapi.json snapshot [skip ci] 2026-08-18 22:25:31 +00:00
README.md feat!: drop the release changelog feed; announcements are the only what's-new surface 2026-08-18 21:34:11 +02:00
renovate.json ci(renovate): use fast-forward automerge (repos disallow rebase-merge) 2026-07-03 16:20:26 +02:00
rust-toolchain.toml deps: update rust to v1.97.1 2026-07-19 00:00:54 +00:00
SELFHOSTING.md docs: configuration is database-managed, not env 2026-08-16 17:56:59 +02:00

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.