Skip to content

Badge

Try the interactive example ↓

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.
  • Access starts private to the creator in the organization’s hidden root workspace. Optional workspace_id and access_visibility: "workspace" use an existing workspace’s grants. The badge’s versions share one permission root; readers can inspect/render, editors can append versions and change lifecycle, and managers can share it.
  • Public rendering is separate: visibility: "public" permits anonymous SVG output, without making source definitions or administration public.

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/0199e100-0000-7000-8000-000000000008.svg?state=failing"
# content-type: image/svg+xml
# cache-control: private, no-store

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": "0199e100-0000-7000-8000-bd935d3c64f4",
    "template_id": "0199e100-0000-7000-8000-000000000008",
    "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: 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 --filter 'status[eq]=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/0199e100-0000-7000-8000-000000000008.svg?state=passing"

While the template is private the same URL is 404 template_not_found - existence is not leaked. Authenticated reads and lists remain permission scoped. A public SVG does not grant access to the template source, its metrics, or lifecycle. Platform administrator claims do not bypass private resource grants.

To share source access with another organization member:

snug access grant --target badge-templates --id "$TEMPLATE_ID" --subject-kind user --subject-id "$USER_ID" --role reader

Use the existing Access CLI’s subject options for groups and service accounts. Grant editor for lifecycle/version management or manager for sharing authority. The same grants apply to all versions; revocation is checked before cached output.

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. Raw KV/leaderboard/remote-config references require a current tenant administrator and a t:<current-tenant>: key prefix. Literal and composite sources do not grant raw Redis access. Source records are private unless shared through badge-sources or access_visibility: "workspace".

Every template binding requires source read permission. For anonymous rendering, the source must additionally opt in with "public_render": true; otherwise publication is rejected. Sources are checked again before parameter overrides or render-cache lookup. This is explicit approval to publish the source’s resolved output, not access to its configuration.

For example:

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": "0199e100-0000-7000-8000-000000000009"} in a template's state_sources lets renders work with no parameters at all: 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 the internal cache and public SVG cache headers. Authenticated SVGs use Cache-Control: private, no-store so shared HTTP caches cannot leak a private render. 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

Storage and tenant boundaries

PostgreSQL owns templates, versions, state-source definitions, active-version selection and quota enforcement. Creating a version and activating it commit atomically. Redis holds disposable render caches, metrics and the live values referenced by configured raw sources. Losing the render cache does not lose the badge or its public URL. New IDs use UUIDv7.

Omitting workspace fields requires no organization setup beyond normal sign-in. For a named workspace, add "workspace_id": "..." and optionally "access_visibility": "workspace" to the JSON file passed to template-add or source-add. Anonymous publication remains controlled by the separate visibility.

Phrase, prefix, contains and one-edit fuzzy name filters remain available through the list API. PostgreSQL migrations install the declared fuzzystrmatch extension for fuzzy matching; filter values are bound parameters and remain permission scoped. Phrase search uses English text-search normalization; fuzzy search matches words within one edit.

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.

Reference

Identifiers

Template and state-source IDs are opaque UUIDv7 strings. Template versions remain integers, and placeholder names remain user-defined keys.

Interactive example

Render values as SVG badges that can appear in a README or dashboard. Templates control the labels, colors, and data each badge displays.

An illustrative example that runs in your browser. No account or API calls required.