Small backend for my VPS. https://tomplanche.com/music
  • Rust 98.9%
  • JavaScript 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-16 16:21:55 +02:00
deploy [46] (feat on main) Add /status endpoint 2026-09-16 16:21:55 +02:00
migration [37] (feat on brew-track) Add brew endpoint 2026-02-18 12:09:36 +01:00
src [46] (feat on main) Add /status endpoint 2026-09-16 16:21:55 +02:00
.env.exemple [41] (feat on github-stats) Add last 6 updated repo stats + /stats endpoint 2026-03-02 19:00:48 +01:00
.gitignore [23] (feat on switch-to-postgres) Switched to SeaORM 2025-10-06 21:37:45 +02:00
build.rs [12] (feat on source-endpoint) Added /source get endpoint 2025-06-14 20:00:31 +02:00
Cargo.lock [46] (feat on main) Add /status endpoint 2026-09-16 16:21:55 +02:00
Cargo.toml [46] (feat on main) Add /status endpoint 2026-09-16 16:21:55 +02:00
ecosystem.config.cjs [45] (feat on main) Add clean pm2 script 2026-09-08 18:59:45 +02:00
README.md [46] (feat on main) Add /status endpoint 2026-09-16 16:21:55 +02:00

vps-back

Rust/Axum backend running on my VPS. Handles analytics tracking, location stickers, and Homebrew bottle download counting. PostgreSQL via SeaORM.

Setup

cp .env.example .env
sea-orm-cli migrate up
cargo run

Configuration

Variable Required Default
API_KEY yes —
DATABASE_URL yes —
HOST no 127.0.0.1
PORT no 8000
ALLOWED_ORIGINS no http://localhost:3000,http://localhost:5173
RUST_LOG no info
STATIC_DIR no static
GITHUB_TOKEN no —
MIAOU_URL no https://miaou.tomplanche.com

DATABASE_URL format: postgresql://user:password@host:port/dbname

Endpoints

Public

GET / — health check

GET /status: status report, served on its own subdomain (status.tomplanche.com). Plain text by default, JSON when the Accept header asks for it (application/json, text/json).

$ curl https://status.tomplanche.com
vps-back status: ok

version    2.0.1
started    2026-09-15T10:12:00+00:00
uptime     23h 24m 13s

checks
  database      ok            3 ms
  miaou         ok            42 ms
  github-stats  ok            updated 2m 13s ago
{
  "data": {
    "status": "ok",
    "version": "2.0.1",
    "started_at": "2026-09-15T10:12:00+00:00",
    "uptime_seconds": 84253,
    "checks": {
      "database": { "status": "ok", "latency_ms": 3 },
      "miaou": { "status": "ok", "latency_ms": 42 },
      "github_stats": { "status": "ok", "age_seconds": 133 }
    }
  }
}

Checks:

Check What it does Statuses
database pings PostgreSQL and measures the round trip ok, down
miaou GETs MIAOU_URL (10 s timeout); 2xx and 3xx count as up ok, down
github_stats reads last_updated_at in github-stats.json; the file is refreshed every 5 minutes ok, stale, unavailable

The top level status is ok only when every check passes, degraded otherwise.

The HTTP code reports this API alone: 503 when the database is unreachable, 200 otherwise. So an uptime monitor watching the HTTP code is not woken up by miaou being down, while the body still shows exactly what is broken.

GET /static/<path> — static files (LastFM fetcher output, github-stats.json)

GET /brew/track/:project/:filename — records a Homebrew bottle download and redirects (302) to the real GitHub release asset. Used by setting root_url in the formula's bottle block:

bottle do
  root_url "https://your-server.com/brew/track/rona"
  sha256 cellar: :any_skip_relocation, arm64_sequoia: "<sha>"
  sha256 cellar: :any_skip_relocation, sequoia:       "<sha>"
  sha256 cellar: :any_skip_relocation, x86_64_linux:  "<sha>"
end

GET /brew/stats — download counts grouped by project and version:

{
  "data": {
    "rona": {
      "total_downloads": 120,
      "total_installs": 120,
      "2.17.7": { "downloads": 80, "installs": 80 }
    }
  }
}

GET /stats/github — 6 most recently updated repos (owner + collaborator), generated every 5 minutes from GITHUB_TOKEN. Returns null if the token is not set or the file has not been written yet:

{
  "data": [
    {
      "name": "rona",
      "link": "https://github.com/rona-rs/rona",
      "stars": 2,
      "owner": "rona-rs",
      "description": "A powerful CLI tool",
      "commits": {
        "total": 293,
        "last": { "hash": "faf085d", "link": "https://github.com/...", "date": "2025-01-01T00:00:00Z" }
      }
    }
  ]
}

GET /stats/brew — download counts grouped by project and version (same data as /brew/stats).

GET /stats/sources — all analytics source counters, name → count:

{ "data": { "twitter": 42, "github": 17 } }

Protected

All /secure/* routes require x-api-key: <key> in the request headers.

Method Path Description
GET /secure/source All sources and their hit counts
POST /secure/source Increment a source counter
GET /secure/stickers All stickers, newest first
GET /secure/stickers/:id Single sticker
POST /secure/stickers Create a sticker

Paginated endpoints accept ?page=1&limit=20 (max limit: 100) and include a _metadata field in the response.

Database

sea-orm-cli migrate up       # apply pending
sea-orm-cli migrate status   # check state
sea-orm-cli migrate down     # roll back one
sea-orm-cli migrate refresh  # reset and re-run all

Tables: sources, stickers, brew_downloads.

Deployment

The status endpoint is exposed on its own subdomain through Caddy. The site block lives in deploy/Caddyfile.example: copy it into the VPS Caddyfile and reload Caddy. It rewrites every path to /status, so the whole subdomain answers the report.

miaou.tomplanche.com is checked through its public URL, so the check covers Caddy and the 10.10.0.2:80 upstream behind it, not just the container. Point MIAOU_URL somewhere else (http://10.10.0.2:80 for instance) to check the upstream directly.

Development

cargo check              # type-check
cargo clippy             # lint
cargo test               # tests
cargo build --release    # production build
RUST_LOG=sqlx::query=debug cargo run  # with SQL logging

License

MIT