Games Backstages

Image optimization service for web games. Upload originals at full quality; the service precomputes lighter variants (AVIF and WebP, three size tiers), serves them as static files cacheable forever, and groups them into zip bundles downloadable in a single request. A JavaScript SDK picks the right variant for the player's device and network.

Base URL: https://games-backstages.com — Web interface: / (account required) — This page: /docs

Concepts

Public read (no authentication)

CORS open (Access-Control-Allow-Origin: *): usable directly as WebGL textures.

RouteResponse
GET /p/{project}/manifest.jsonProject index (see below). Cache-Control: no-cache.
GET /p/{project}/{ref}.{hash}.{tier}.{format}One variant (format = avif | webp). Immutable, cached 1 year.
GET /p/{project}/_bundles/{group}.{hash}.{tier}.{format}.zipAll images of a group in one zip (uncompressed, entries {ref}.{format}). Immutable.
GET /i/{project}/{ref}?tier={tier}&format={avif|webp|auto}302 to the variant. Defaults: mid, auto (AVIF if the Accept header allows it). Stable URL for an <img>.
GET /sdk/backstages.jsSDK (ES module), types: /sdk/backstages.d.ts.

Manifest

{
  "project": "shardball",
  "updatedAt": 1790409039586,
  "base": "/p/shardball/",
  "pattern": "{name}.{hash}.{tier}.{format}",
  "bundlePattern": "_bundles/{group}.{hash}.{tier}.{format}.zip",
  "tiers": { "high": { "scale": 1, "maxSide": 4096, "webp": 85, "avif": 60 }, "mid": { … }, "low": { … } },
  "formats": ["avif", "webp"],
  "images": {
    "bonus/bomb": {
      "hash": "250ce049cf", "w": 1254, "h": 1254, "bytes": 1172804,
      "variants": { "high": { "w": 1254, "h": 1254, "avif": 39170, "webp": 73536 }, "mid": { … }, "low": { … } }
    }
  },
  "bundles": {
    "bonus": { "hash": "dd73691661", "images": ["bonus/bomb", "…"], "bytes": { "mid": { "avif": 87042, "webp": 163084 }, … } }
  }
}

Variant URL: base + pattern; bundle URL: base + bundlePattern. bytes = size in bytes; w/h in pixels.

Write (API key)

Each project has an API key, visible and regenerable in the web interface (project page). Header: Authorization: Bearer {key}. Regenerating the key revokes the old one immediately.

RouteEffectResponse
GET /api/projects/{project}Reads the manifest.200 manifest
GET /api/projects/{project}/images/{ref}Downloads the original file (private).200 image
PUT /api/projects/{project}/images/{ref}
body = raw image file
Creates or replaces the image, computes all variants and bundles (a few seconds). PNG, JPEG, WebP, AVIF, TIFF, GIF; 50 MB max.201 {changed: true, image}
200 {changed: false, image} if identical
POST /api/projects/{project}/images/{ref}?…Edits the original in one pass, then re-processes once. Applied in this order:
1. op= comma-separated, in order: trim (crop to visible pixels, else to the uniform border), center (center the subject in its transparent frame, same size), rotr/rotl (90° right/left), flop/flip (horizontal/vertical mirror);
2. angle: free rotation, −45 to 45° (clockwise if positive), bilinear. fit=crop (default): cropped to the largest rectangle with the image's aspect ratio (no empty corners); fit=adapt: the whole image stays visible (frame enlarged, transparent corners, then tightened to visible pixels);
3. crop=left,top,right,bottom: manual crop, margins removed as fractions (0–1) of the image at this stage;
4. scale: downscale factor, 0.01–1, aspect ratio kept (area averaging, alpha-weighted);
5. color: saturation, contrast, brightness (multipliers 0–3, 1 = unchanged), hue (−180 to 180°).
Example: ?op=trim,rotr&angle=-7.5&scale=0.5&hue=30. The math is public in /pixels.js.
200 {changed, image}
DELETE /api/projects/{project}/images/{ref}Deletes the image.204
DELETE /api/projects/{project}/folders/{folder}Deletes every image in the folder.200 {deleted: n}

image = {name, hash, ext, w, h, bytes, variants, updatedAt}.

curl -X PUT -H "Authorization: Bearer $KEY" --data-binary @bomb.png \
  https://games-backstages.com/api/projects/my-game/images/bonus/bomb

Errors

JSON body {"error": "message"}. 400 invalid name or parameter · 401 missing or invalid key · 404 unknown image, folder or project · 413 file too large · 415 unsupported format.

SDK (browser)

import { createAssets } from 'https://games-backstages.com/sdk/backstages.js';

const assets = createAssets({ base: 'https://games-backstages.com', project: 'my-game' });
await assets.ready;                      // reads the manifest
await assets.loadGroup('bonus');         // one zip for the whole folder; also loadAll(), loadImage(ref)
const bmp = assets.get('bonus/bomb');    // ImageBitmap (e.g. PIXI.Texture.from(bmp))
const url = assets.url('bonus/bomb');    // URL of the chosen variant

Options: tier (auto | low | mid | high), format (auto | avif | webp), decode (default createImageBitmap). With auto, the tier depends on the screen (size × pixel density), device memory and data-saver mode, then drops one step if the measured throughput is low; the format is AVIF if the browser can decode it.

Accounts

No public sign-up: an administrator creates accounts. An account owns its projects and creates or deletes them from the web interface. Session routes (/api/login, /api/projects…) serve the interface; for automation, use a project's API key.