ServicesBadge

Badge

Data-to-image rendering: register versioned SVG templates with typed placeholders, bind them to live state, and serve cacheable image/svg+xml badges - over the authenticated API or anonymously at a public URL - with optional Ed25519-signed metadata proving what the badge said and when.

When to reach for it: build and service-status badges for READMEs and dashboards, leaderboard rank badges on player profiles, social-share proof images, agent-verifiable state receipts read from signed metadata.

When not to: tracking whether a user earned something belongs to Achievements - Badge renders images from state, it does not track progress. Hosting static images belongs to Blob.

Concepts

  • Templates are SVG documents with handlebars-style {{slots}}, created as version 1 and extended by appending versions. SVG is sanitized on write - <script, javascript:, event handlers are 400 invalid_svg.
  • Placeholders are typed - string, number, enum, color - with optional defaults and a required flag. Only declared placeholders are substituted; an undeclared {{slot}} passes through as literal text.
  • Thresholds are ordered rules over the resolved state; the first matching when (e.g. $.state == 'passing') sets display variables like colors, so callers pass data and the template picks looks.
  • State sources are registered lookups (KV, leaderboard, remote config, literal) that feed placeholders server-side with live state.
  • Lifecycle per version: draft -> active -> deprecated / disabled. Renders resolve the active version unless pinned; disable is the only state that rejects renders, and it is reversible.
  • Visibility gates only the anonymous route: public renders unauthenticated at the server root; private does not hide a template from other authenticated users in the same tenant.

Create a template

snug badge template-add --file status-badge.json --activate takes a JSON CreateTemplateRequest; --activate skips the draft stage:

{
  "name": "docs4.build-status",
  "svg_template": "<svg xmlns=\"...\" width=\"120\" height=\"20\">...fill=\"{{color}}\"...{{label}}...{{message}}...</svg>",
  "placeholders": [
    {"name": "label", "type": "string", "default": "build"},
    {"name": "state", "type": "enum", "values": ["passing", "failing"], "required": true},
    {"name": "message", "type": "string", "default": "unknown"},
    {"name": "color", "type": "color", "default": "#9f9f9f"}
  ],
  "thresholds": [
    {"when": "$.state == 'passing'", "set": {"color": "#2da44e", "message": "passing"}},
    {"when": "$.state == 'failing'", "set": {"color": "#d73a49", "message": "failing"}}
  ],
  "cache": {"ttl_seconds": 30},
  "verification": {"signed_metadata": true}
}

Without --activate the template stays draft; the owner can still render it (with "preview": true to skip cache writes) before activating.

Render

The CLI renders by id: snug badge render --id XHSb... --param state=passing --output badge.svg. Over HTTP, GET /api/v1/badges/render/{template_id}.svg returns the raw image - the .svg suffix is optional, and every query parameter except version becomes a render parameter:

curl -H "Authorization: Bearer $SNUG_API_TOKEN" \
  "$SNUG_API_URL/api/v1/badges/render/XHSbLagCVnnPAZvejzAu.svg?state=failing"
# content-type: image/svg+xml
# cache-control: public, max-age=30

POST /api/v1/badges/render returns JSON with the SVG plus cache detail and the signed metadata (captured live, trimmed):

{
  "status": 200,
  "msg": "OK",
  "data": {
    "render_id": "UnPZqrTUuybHqHvDgRJL",
    "template_id": "XHSbLagCVnnPAZvejzAu",
    "template_version": 1,
    "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...>...</svg>",
    "state_hash": "sha256:ca0aeea44cb44f96...",
    "cache": { "hit": true, "ttl_seconds": 30 },
    "verification": { "signature_alg": "ed25519", "signature": "Vqc99EBUeL7...", "...": "..." }
  }
}

Validation is strict, verified live: a missing required placeholder is 400 missing_placeholder, a value outside an enum's list is 400 invalid_placeholder, and an unknown id is 404 template_not_found.

Versions and lifecycle

snug badge template-version XHSb... --file status-v2.json --activate
snug badge render --id XHSb... --version 1 --param state=passing   # pin the old version
snug badge template-deprecate XHSb... --version 1
snug badge template-disable XHSb... --version 1
snug badge template-activate XHSb... --version 1                   # reverses disable

Deprecated versions still render when pinned; a disabled version fails with 403 template_disabled while the active version keeps working. Note template-list returns one row per version (a two-version template appears twice); filter with --status active for current versions.

Public badges

Templates start private. Flipping one public enables the anonymous route, which lives at the server root, not under /api/v1:

snug badge template-visibility XHSb... --visibility public
curl "$SNUG_API_URL/badges/public/render/XHSbLagCVnnPAZvejzAu.svg?state=passing"

While the template is private the same URL is 404 template_not_found - existence is not leaked. Reads, renders, and listing are tenant-shared regardless of visibility; lifecycle and visibility changes are owner-or-admin only, otherwise 403 insufficient_permissions (verified with a second user's token).

State sources

A state source resolves a placeholder server-side. kind is literal, kv, leaderboard, remote_config, or composite; the key_template is rendered from the request parameters and addresses the other service's backing storage key verbatim (tenant prefix included), so wiring one up is an operator-level task:

snug kv set docs4/service-health '"passing"'
snug badge source-add --file source.json    # {"kind": "kv", "key_template": "t:TDxB...:kv:j:docs4/service-health", "fallback": {"value": "unknown"}}
snug badge source-list

Binding {"placeholder": "state", "source_id": "BYxc..."} in a template's state_sources lets renders work with no parameters at all - verified live: the badge flipped from green to red after snug kv set docs4/service-health '"failing"' once the cache TTL lapsed. Two verified gotchas: a caller-supplied parameter overrides the bound source for that placeholder, and fallbacks go through normal placeholder validation - a fallback outside an enum's values is 400 invalid_placeholder when the source is missing.

Cache and metrics

Rendered output is cached by template, version, resolved state, and metadata mode; cache.ttl_seconds drives both the cache and the Cache-Control header. Purge after out-of-band changes:

snug badge cache-purge XHSb...       # {"purged_entries": 2, ...}
snug badge template-metrics XHSb...  # render_count, cache_hit_ratio, p50/p95 latency, render_errors by class, verification_failures

Limits and configuration

Templates up to 128 KB, rendered output up to 256 KB, 50 placeholders and 20 state sources per template, cache TTL capped at 300 seconds (default 60) - all tunable via the BADGE_* variables in the Badge CONFIG reference.

Reference

On this page