go-doc/data/docs/go-bananas-api.md
2026-10-07 15:05:48 +02:00

13 KiB

Imported from docs/api.md in the go-bananas repository.


HTTP API

Two surfaces with different postures:

/api/v1 /rt/v1
Audience Administrators Applications and browsers
Authentication Required None, by design
Caching no-store Aggressive, immutable by URL
Audit Every mutation No
Rate limit Generous Applied

A theming service that required a credential could not be consumed from a <link> tag, which is the primary use case.


Conventions

  • All bodies are JSON. One shape, everywhere.
  • Errors: { code, message, details?, requestId? }. code is a stable machine-readable identifier and is part of the contract.
  • requestId is on every error and in X-Request-ID on every response.
  • Timestamps are RFC 3339 in UTC.
  • Every mutable request accepts baseVersion for optimistic concurrency.
Code Status Meaning
bad_request 400 Malformed
validation_failed 400 Invalid, with per-field details
unauthorized 401 No usable credential
forbidden 403 Insufficient role
not_found 404 Missing, or deliberately not disclosed
conflict 409 Uniqueness, or an optimistic-concurrency conflict
immutable 409 A built-in theme cannot be deleted
rate_limited 429 With Retry-After
internal_error 500 Unexpected

Runtime API

GET /rt/v1/theme.css

A theme's stylesheet.

Parameter Required Default Purpose
slug Yes — The theme
appearance No The theme's default light or dark
variant No full full, tokens, minified
v No Latest Pin a version
curl 'https://themes.example.com/rt/v1/theme.css?slug=midnight'
curl 'https://themes.example.com/rt/v1/theme.css?slug=midnight&appearance=dark'
curl 'https://themes.example.com/rt/v1/theme.css?slug=midnight&variant=tokens'
curl 'https://themes.example.com/rt/v1/theme.css?slug=midnight&v=7'
HTTP/1.1 200 OK
Content-Type: text/css; charset=utf-8
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
ETag: "a1f4b5833f60d17d42328a90213b20f6"
X-Theme-Version: 7
X-Theme-Updated-At: 2026-10-04T09:14:27Z
Vary: Accept-Encoding

A draft theme returns 404, not 403, so a probe cannot confirm it exists.

GET /rt/v1/theme.json

Resolved tokens.

{
  "theme": {
    "slug": "midnight",
    "name": "Midnight",
    "version": 7,
    "appearance": "dark",
    "defaultAppearance": "dark",
    "updatedAt": "2026-10-04T09:14:27Z"
  },
  "variables": {
    "--bn-color-semantic-primary": "#7c6bf5",
    "--bn-color-on-primary": "#0f0d1f",
    "--bn-radius-lg": "0.75rem",
    "--bn-space-4": "1rem"
  },
  "tokens": {
    "color": { "semantic": { "primary": { "$type": "color", "$value": "#7c6bf5" } } }
  },
  "meta": { "tags": ["dark", "violet"] }
}

variables is a flat map for anything reading values. tokens is the nested DTCG document for mapping onto component props.

GET /rt/v1/manifest.json

The discovery document for building a picker. Deliberately small: no token data.

{
  "apiVersion": "1",
  "default": "midnight",
  "baseUrl": "https://themes.example.com/rt/v1",
  "themes": [
    {
      "slug": "midnight",
      "name": "Midnight",
      "description": "Violet-accented dark interface.",
      "defaultAppearance": "dark",
      "version": 7,
      "tags": ["dark", "violet"],
      "css": "https://themes.example.com/rt/v1/theme.css?slug=midnight",
      "tokens": "https://themes.example.com/rt/v1/theme.json?slug=midnight"
    }
  ]
}

GET /rt/v1/embed

A ready-to-paste snippet. ?target=html (the default, a full preview page), react, or javascript.

SDK bundles

GET /rt/v1/runtime.js, /react.js, /global.js, /embed.js. Immutable, served with a one-year Cache-Control, and CORS-open, since they are public assets loaded cross-origin.

Path-addressable forms

/rt/v1/themes/{slug}/theme.css, /rt/v1/themes/{slug}/theme.json, /rt/v1/themes/{slug}/embed. For consumers that prefer REST shapes or a CDN configured with path patterns rather than query strings.


Administration API

Requires a session cookie. Every mutation is audited.

There is no machine credential: API keys were removed. The runtime API is public by design, and the administration API is for people, so a bearer token is not a credential here and a request carrying one is treated as unauthenticated.

Auth

Method Path Purpose
GET /api/v1/auth/me The current session. Always 200
GET /api/v1/auth/config Client-visible configuration
GET /auth/login Start the OIDC flow
GET /auth/callback Complete it
GET/POST /auth/logout End the session and the provider session

/auth/me always answers 200 with authenticated: false when there is no session, because "not logged in yet" is the normal state on first load and not an error the UI should handle as one.

{
  "authenticated": true,
  "mode": "oidc",
  "identity": {
    "subject": "a1b2-c3d4",
    "email": "designer@example.com",
    "name": "A Designer",
    "roles": ["editor"],
    "method": "oidc"
  },
  "permissions": {
    "read": true, "write": true, "publish": true,
    "delete": false, "manageKeys": false, "manageUsers": false, "manageSettings": false
  },
  "preferences": {
    "selectedTheme": "midnight",
    "selectedAppearance": "dark"
  }
}

preferences is present only when authenticated is true. It rides along with the session so the UI can preselect a theme from the same round trip that establishes who the caller is, rather than making a second request before it can paint.

Preferences

Per-account interface preferences: which published theme the person has chosen, and which of its two colour schemes to render. Any authenticated role may read and write these, because a preference only affects what that one person sees and is keyed to their own subject.

Method Path Purpose
GET /api/v1/me/preferences The caller's preference, or empty when none is stored
PUT /api/v1/me/preferences Record a preference and echo back what was stored
DELETE /api/v1/me/preferences Forget it, returning the interface defaults
PUT /api/v1/me/preferences
{"selectedTheme": "midnight", "selectedAppearance": "dark"}
{"selectedTheme": "midnight", "selectedAppearance": "dark"}

Both fields empty means nothing has been chosen, and that is a valid state rather than a missing one: the client applies its own default. An empty selectedTheme clears the choice, and a selectedAppearance sent with it is dropped rather than stored, because a scheme has nothing to select from.

selectedTheme names a published theme rather than copying it, so editing or unpublishing that theme shows through everywhere without touching a stored value. Two consequences are worth knowing:

  • Saving a draft theme slug is a 400. Drafts are invisible to runtime consumers, so a preference pointing at one would leave the UI rendering a theme it cannot load.
  • A preference naming a theme that has since been deleted or archived reads back as no choice, so a stale value cannot pin the interface to something that no longer exists. The row is left alone; only the read is filtered.

The response is authoritative and should be applied in place of the request. It is the only way a client learns that its value was filtered or cleared.

Preferences are deliberately not claims in the session token. The token is issued by the identity provider and this service does not control it, so a choice made here has to be read back from this API. They are also stored per account rather than per browser, so they survive a re-login (UpsertUser rewrites the users projection on every login) and follow the person to another device.

Themes

Method Path Role
GET /api/v1/themes viewer
GET /api/v1/themes/{slug} viewer
GET /api/v1/themes/{slug}/export viewer
POST /api/v1/themes editor
PATCH /api/v1/themes/{slug} editor
POST /api/v1/themes/{slug}/publish editor
POST /api/v1/themes/{slug}/archive editor
POST /api/v1/themes/{slug}/duplicate editor
POST /api/v1/themes/import editor
DELETE /api/v1/themes/{slug} admin

GET /api/v1/themes takes q, status, system, tag, limit, offset, sort (updated, created, name, slug) and order. An unrecognised sort falls back to the default rather than reaching SQL.

Listings omit token documents. They are the bulk of each row and a listing never needs them.

curl -X POST https://themes.example.com/api/v1/themes \
  -H 'Content-Type: application/json' \
  -b cookies.txt \
  -d '{
    "name": "Acme Product",
    "slug": "acme",
    "description": "The default theme for customer-facing applications",
    "status": "draft",
    "tokens": { "color": { "semantic": { "primary": { "$type": "color", "$value": "#4f46e5" } } } }
  }'
curl -X PATCH https://themes.example.com/api/v1/themes/acme \
  -H 'Content-Type: application/json' -b cookies.txt \
  -d '{ "tokens": { … }, "baseVersion": 3, "message": "rebalanced the neutrals" }'

baseVersion gives optimistic concurrency. If the theme has moved on, the write is rejected with 409 and a message naming both versions, rather than silently overwriting someone else's work.

Publishing validates first:

{
  "code": "validation_failed",
  "message": "the theme cannot be published because its tokens are invalid",
  "details": [
    { "field": "color.semantic.primary", "message": "not a valid colour: \"#nothex\"" }
  ]
}

History

Method Path Role
GET /api/v1/themes/{slug}/versions viewer
GET /api/v1/themes/{slug}/versions/{version} viewer
GET /api/v1/themes/{slug}/diff?from=1&to=2 viewer
POST /api/v1/themes/{slug}/versions/{version}/rollback editor

Rollback appends: the snapshot becomes the current state as a new version. History is never rewritten, so an application deployed against version 7 can never have version 7 taken away.

{
  "fromVersion": 6,
  "toVersion": 7,
  "added": ["color.semantic.tertiary"],
  "removed": [],
  "changed": [
    { "path": "color.semantic.primary", "from": "#4f46e5", "to": "#7c6bf5" }
  ],
  "metaChanged": false
}

Token tooling

Read-only analysis of a submitted document. Any authenticated user may call these.

Method Path Purpose
POST /api/v1/tokens/validate Validate without saving
GET /api/v1/tokens/schema The group and kind registry
GET /api/v1/tokens/palette?hue=&saturation=&lightness=&hueSpread= Generate a ramp
GET /api/v1/tokens/contrast?fg=…&bg=… WCAG contrast
{
  "valid": false,
  "tokenCount": 214,
  "issues": [
    { "path": "color.semantic.primary", "severity": "error", "message": "not a valid colour: \"#nothex\"" },
    { "path": "$appearances", "severity": "warning", "message": "no dark appearance overrides are defined", "hint": "add {\"$appearances\":{\"dark\":{…}}}" }
  ]
}
{
  "foreground": "#7c6bf5",
  "background": "#1a1d23",
  "ratio": 6.42,
  "rounded": 6.42,
  "level": "aa",
  "thresholds": { "aaLarge": 3, "aa": 4.5, "aaa": 7 },
  "bestForeground": "#ffffff"
}

Administration

Method Path Role
GET /api/v1/stats viewer
GET /api/v1/admin/users admin
GET /api/v1/admin/audit admin
GET /api/v1/admin/settings admin
PUT /api/v1/admin/settings admin
POST /api/v1/admin/cache/rebuild admin

Creating a key returns the plaintext once:

{
  "key": { "id": "key_x7f2", "name": "CI publish", "prefix": "gb_a1b2c3d4", "scopes": ["themes:write"] },
  "secret": "gb_a1b2c3d4_…",
  "warning": "This secret is shown once and cannot be recovered. Store it now."
}

Operational endpoints

Path Purpose
/livez, /healthz Liveness, with build information on /healthz
/readyz Readiness. Checks the database
/metrics Prometheus, when config.enableMetrics
{
  "status": "ready",
  "checks": {
    "database": { "status": "ok" },
    "themes": { "status": "ok", "count": 42, "databaseBytes": 212992 },
    "cache": { "status": "ok", "entries": 24 }
  }
}