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 are400 invalid_svg. - Placeholders are typed -
string,number,enum,color- with optional defaults and arequiredflag. 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:
publicrenders unauthenticated at the server root;privatedoes 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=30POST /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 disableDeprecated 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-listBinding {"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_failuresLimits 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
- Badge API - every endpoint, callable
- Signed badges and verification - Ed25519 receipts
- Related: Achievements for earning logic, KV Store and Leaderboard as state sources