openapi: 3.1.0 info: title: go-bananas version: 1.0.0 summary: Runtime design-token service. description: 'Author design-token themes and serve them to any application at run time. ## Two surfaces, two postures `/rt/v1` is the **runtime** API: public, aggressively cacheable, and designed to be consumed from a `` tag with no credential. A theming service that required a credential could not be consumed the way a theme is actually consumed. `/api/v1` is the **administrative** API: authenticated, never cached, and every mutation audited. ## Versioning Both surfaces are independently versioned. Adding an optional field, a new endpoint or a new query parameter is not a breaking change. Changing the meaning of an existing one is, and gets a new major version served alongside the old one for at least one cycle.' license: name: MIT identifier: MIT contact: name: urpi platform team url: https://github.com/urpi/go-bananas servers: - url: '{origin}' description: This deployment. variables: origin: default: https://themes.example.com description: The externally reachable origin. tags: - name: runtime description: Public. Consumed by applications and browsers. - name: themes description: Theme management. Requires authentication. - name: history description: Version history, diffing and rollback. - name: tokens description: Read-only analysis of a token document. - name: auth description: Session introspection and the login flow. - name: preferences description: The caller's own interface preferences. Any authenticated role. - name: admin description: Keys, users, settings and the audit log. Admin role. - name: operational description: Probes and metrics. paths: /rt/v1/theme.css: get: summary: A theme stylesheet description: 'Returns the theme rendered as CSS custom properties, plus a small component layer in `@layer bananas`, so an application''s own unlayered CSS always wins the cascade. Responses are **immutable by URL**: publishing a theme changes its version, and a version-pinned URL never changes. That lets a CDN cache them hard. `If-None-Match` and `If-Modified-Since` are both honoured, so a redeploy costs a revalidation and no body. A draft theme returns **404 rather than 403**, so a probe cannot confirm that it exists.' tags: - runtime responses: '200': description: The stylesheet. content: text/css: schema: type: string example: "[data-bananas-theme=\"midnight\"] {\n --bn-color-semantic-primary: #7c6bf5;\n\ }" headers: &id001 ETag: description: A strong validator for the response. schema: type: string X-Theme-Version: description: The version served. schema: type: string X-Theme-Updated-At: description: When it was published. schema: type: string Cache-Control: description: The caching policy applied. schema: type: string X-Request-ID: description: Correlation id for the server log. schema: type: string '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: query required: true description: The theme slug. schema: type: string - name: appearance in: query required: false description: The colour scheme. Defaults to the theme's own default, which is what makes a dark-first theme correct without the parameter being passed. schema: type: string enum: - light - dark - name: variant in: query required: false description: '`full` includes the component layer, `tokens` omits it, `minified` strips comments for production.' schema: type: string enum: - full - tokens - minified - name: v in: query required: false description: 'Pin a version. This URL never changes, which is what makes a safe deploy possible: build against version 7 and the render cannot change underneath you.' schema: type: integer minimum: 1 security: [] /rt/v1/theme.json: get: summary: A theme's resolved tokens description: Carries both a flat custom-property map and the nested DTCG document, so a consumer can read either without a transformation step. Derived tokens such as the `on-*` foreground pairs and the `-hover` / `-active` interaction states are included. tags: - runtime responses: '200': description: The resolved theme. content: application/json: schema: $ref: '#/components/schemas/ResolvedTheme' headers: *id001 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: query required: true description: The theme slug. schema: type: string - name: appearance in: query required: false description: The colour scheme. Defaults to the theme's default. schema: type: string enum: - light - dark security: [] /rt/v1/manifest.json: get: summary: The theme manifest description: 'The discovery document a loader fetches once to build a theme picker. Deliberately small: no token data, so a page with a picker does not download every theme. Cached briefly, because it changes whenever a theme is published.' tags: - runtime responses: '200': description: The manifest. content: application/json: schema: $ref: '#/components/schemas/Manifest' headers: Cache-Control: description: Short-lived. schema: type: string '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /rt/v1/embed: get: summary: An integration snippet description: 'Generates the markup to paste, from this deployment''s own configuration, so it cannot drift from reality. Always answers JSON, whatever the target: the admin UI fetches this with a JSON parser and an HTML response for the default target is what made the Integrate page report that it could not parse the reply. The slug must name an existing theme, and every interpolated value is escaped for the target language, because the output is pasted into someone else''s application.' tags: - runtime responses: '200': description: The snippet. content: application/json: schema: $ref: '#/components/schemas/EmbedSnippet' '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: No such theme. content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: query required: false description: The theme to generate for. schema: type: string - name: target in: query required: false description: The integration shape. schema: type: string enum: - html - react - javascript security: [] /rt/v1/themes/{slug}/theme.css: get: summary: A theme stylesheet, by path description: A REST-shaped equivalent of `/rt/v1/theme.css`, for consumers that prefer a path to a query string, or a CDN configured with path patterns. tags: - runtime responses: '200': description: The stylesheet. content: text/css: schema: type: string headers: *id001 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: [] /rt/v1/themes/{slug}/theme.json: get: summary: A theme's resolved tokens, by path description: A REST-shaped equivalent of `/rt/v1/theme.json`. tags: - runtime responses: '200': description: The resolved theme. content: application/json: schema: $ref: '#/components/schemas/ResolvedTheme' headers: *id001 '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: [] /rt/v1/runtime.js: get: summary: The runtime.js bundle description: 'The framework-free ES module: `applyTheme`, `setAppearance`, `loadManifest`, `getVariables`, `watchSystemAppearance`, `subscribe`. Served from this origin so a consuming application needs no build step and no npm dependency. React is imported as a **bare specifier** and resolved by the consumer''s own import map, which is what guarantees a single React instance in the page: two copies would each have their own hook dispatcher.' tags: - runtime responses: '200': description: The bundle. content: application/javascript: schema: type: string headers: Cache-Control: description: Immutable. The filename is stable so an import map can name it. schema: type: string security: [] /rt/v1/react.js: get: summary: The react.js bundle description: 'The React provider and hooks: `BananasProvider`, `useBananas`, `useThemeVariable`, `useThemeVariables`, `useAvailableThemes`. Served from this origin so a consuming application needs no build step and no npm dependency. React is imported as a **bare specifier** and resolved by the consumer''s own import map, which is what guarantees a single React instance in the page: two copies would each have their own hook dispatcher.' tags: - runtime responses: '200': description: The bundle. content: application/javascript: schema: type: string headers: Cache-Control: description: Immutable. The filename is stable so an import map can name it. schema: type: string security: [] /rt/v1/global.js: get: summary: The global.js bundle description: 'The same API attached to `window.Bananas`, for a page that cannot use modules. Served from this origin so a consuming application needs no build step and no npm dependency. React is imported as a **bare specifier** and resolved by the consumer''s own import map, which is what guarantees a single React instance in the page: two copies would each have their own hook dispatcher.' tags: - runtime responses: '200': description: The bundle. content: application/javascript: schema: type: string headers: Cache-Control: description: Immutable. The filename is stable so an import map can name it. schema: type: string security: [] /rt/v1/embed.js: get: summary: The embed.js bundle description: 'The one-line script-tag embed. Configured with data attributes. Served from this origin so a consuming application needs no build step and no npm dependency. React is imported as a **bare specifier** and resolved by the consumer''s own import map, which is what guarantees a single React instance in the page: two copies would each have their own hook dispatcher.' tags: - runtime responses: '200': description: The bundle. content: application/javascript: schema: type: string headers: Cache-Control: description: Immutable. The filename is stable so an import map can name it. schema: type: string security: [] /auth/login: get: summary: Begin the login flow description: Redirects to the identity provider. The return path and the PKCE verifier are carried in a signed cookie rather than server-side state, so any replica behind the load balancer can complete the flow. tags: - auth responses: '302': description: Redirect to the provider. '404': description: No identity provider is configured. content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: returnTo in: query required: false description: 'A **local** path to return to. An absolute URL is rejected: it would be an open redirect that also primes a phishing flow.' schema: type: string security: [] /auth/callback: get: summary: Complete the login flow description: Exchanges the authorization code, verifies the ID token signature, issuer, audience and nonce, resolves roles from the user's groups, and sets the session cookie. The transaction state is single-use. tags: - auth responses: '302': description: Redirect to the return path, with a session cookie set. '401': description: The login could not be completed. The message names the reason, because "invalid state" and "user declined" need different actions. content: application/json: schema: $ref: '#/components/schemas/ApiError' '502': description: The provider returned an unexpected response. content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /auth/logout: get: summary: End the session description: Clears the session cookie and, when an OIDC provider is configured, ends the provider session too. tags: - auth responses: '302': description: Redirect, to the provider when configured. security: - sessionCookie: [] - bearerToken: [] post: summary: End the session description: Clears the session cookie and, when an OIDC provider is configured, ends the provider session too. tags: - auth responses: '302': description: Redirect, to the provider when configured. security: - sessionCookie: [] - bearerToken: [] /api/v1/auth/me: get: summary: The current session description: The session, and what the caller may do. tags: - auth responses: '200': description: The session. content: application/json: schema: $ref: '#/components/schemas/Session' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /api/v1/auth/config: get: summary: Client-visible configuration description: What the UI needs before a session exists. Contains no secret material. tags: - auth responses: '200': description: The configuration. content: application/json: schema: $ref: '#/components/schemas/AppConfig' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /api/v1/me/preferences: get: summary: The caller's interface preferences description: Empty when nothing is stored, which is every account until it chooses once. tags: - preferences responses: '200': description: The stored preference. content: application/json: schema: $ref: '#/components/schemas/UserPreferences' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' put: summary: Record the caller's interface preferences description: 'Any authenticated role may call this. The preference is keyed to the caller''s own subject and grants nothing. An empty selectedTheme clears the choice. The response is authoritative: the server rejects a theme that is not published, and reports a stored theme that has since disappeared as no choice. tags: - preferences requestBody: required: true content: application/json: schema: $ref: ' responses: '200': description: What the server stored. content: application/json: schema: $ref: '#/components/schemas/UserPreferences' '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' delete: summary: Forget the caller's preferences description: Returns the interface to its defaults. tags: - preferences responses: '200': description: The empty preference. content: application/json: schema: $ref: '#/components/schemas/UserPreferences' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' /api/v1/themes: get: summary: List themes description: A page of themes, without their token documents. tags: - themes responses: '200': description: A page of themes. content: application/json: schema: $ref: '#/components/schemas/ThemePage' headers: &id002 Cache-Control: &id003 description: '`no-store`. Administration responses are per-user and change on every write.' schema: type: string X-Request-ID: &id004 description: Correlation id for the server log. schema: type: string '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: q in: query required: false description: Matches slug, name or description. LIKE wildcards are escaped, so a literal `%` matches a literal `%`. schema: type: string - name: status in: query required: false description: Filter by publication state. schema: &id005 $ref: '#/components/schemas/ThemeStatus' - name: system in: query required: false description: Filter on the built-in flag. schema: type: boolean - name: tag in: query required: false description: Requires the tag in the metadata. schema: type: string - name: limit in: query required: false description: '' schema: type: integer minimum: 1 maximum: 200 default: 50 - name: offset in: query required: false description: '' schema: type: integer minimum: 0 default: 0 - name: sort in: query required: false description: Resolved through an allow-list. An unrecognised value falls back to the default rather than reaching the SQL statement. schema: type: string enum: - updated - created - name - slug default: updated - name: order in: query required: false description: '' schema: type: string enum: - asc - desc default: desc security: - sessionCookie: [] - bearerToken: [] post: summary: Create a theme description: 'Creates a **draft**. A draft may be incomplete: validation gates publication, not editing, which is what makes building a theme up incrementally practical.' tags: - themes responses: '201': description: The created theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '409': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ThemeMutation' example: name: Acme Product slug: acme description: The default theme for customer-facing applications status: draft tokens: color: semantic: primary: $type: color $value: '#4f46e5' security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}: get: summary: Get a theme description: The theme with its full token document. tags: - themes responses: '200': description: The theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: - sessionCookie: [] - bearerToken: [] patch: summary: Update a theme description: 'A partial update: absent fields are left unchanged. Every write bumps the version, because the version is the render cache key and must change whenever the rendered output could. `baseVersion` gives optimistic concurrency. Without it, two editors silently overwrite each other; with it, the loser gets a 409 naming both versions.' tags: - themes responses: '200': description: The updated theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '409': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ThemeMutation' example: tokens: color: semantic: primary: $type: color $value: '#7c6bf5' baseVersion: 3 message: rebalanced the neutrals security: - sessionCookie: [] - bearerToken: [] delete: summary: Delete a theme description: 'Removes the theme and its history. Refused for a built-in theme with 409: built-ins are immutable so that a consumer pinned to the slug cannot break on upgrade. Duplicate it instead.' tags: - themes responses: '204': description: Deleted. '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '409': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/publish: post: summary: Publish a theme description: 'Validates the token document first: errors block, warnings do not. On success the theme is served to every consuming application within one `s-maxage` window. A built-in theme can be published but not deleted.' tags: - themes responses: '200': description: The published theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/archive: post: summary: Archive a theme description: Hidden from listings and from the runtime manifest. The last published revision stays resolvable for a pinned consumer, so archiving does not break a deployment in progress. tags: - themes responses: '200': description: The archived theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/duplicate: post: summary: Duplicate a theme description: Copies a theme under a new slug as a draft, with its own history starting at version 1. The copy is no longer flagged as built-in, so it can then be deleted. tags: - themes responses: '201': description: The copy. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '409': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The source theme slug. schema: type: string requestBody: required: true content: application/json: schema: type: object required: - slug properties: slug: type: string description: The slug for the copy. example: slug: acme-v2 security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/export: get: summary: Export a theme description: A portable document. Import and export are exact inverses, so migrating a theme between environments is a file copy rather than a project. tags: - themes responses: '200': description: The export. content: application/json: schema: $ref: '#/components/schemas/ThemeExport' headers: Cache-Control: *id003 X-Request-ID: *id004 Content-Disposition: description: A download filename. schema: type: string '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/import: post: summary: Import a theme description: Creates or replaces a theme from a portable document. The document is validated before anything is written, so a malformed import never reaches the database. tags: - themes responses: '201': description: The imported theme. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '409': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ImportRequest' example: slug: acme name: Acme asDraft: true tokens: {} security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/versions: get: summary: A theme's history description: 'Summaries newest first, without token documents: a hundred revisions of a hundred-token theme would otherwise be a very large response for no benefit.' tags: - history responses: '200': description: The history. content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/ThemeVersion' headers: *id002 '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string - name: limit in: query required: false description: '' schema: type: integer minimum: 1 maximum: 200 default: 50 security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/versions/{version}: get: summary: A historical snapshot description: One immutable revision, including its token document. tags: - history responses: '200': description: The snapshot. content: application/json: schema: $ref: '#/components/schemas/ThemeVersion' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string - name: version in: path required: true description: The revision number. schema: type: integer minimum: 1 security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/versions/{version}/rollback: post: summary: Roll back to a revision description: Restores a snapshot as a **new** version rather than rewinding. History is append-only, so an application deployed against version 7 can never have version 7 taken away from it. tags: - history responses: '200': description: The theme after rollback. content: application/json: schema: $ref: '#/components/schemas/Theme' headers: *id002 '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string - name: version in: path required: true description: The revision to restore. schema: type: integer minimum: 1 security: - sessionCookie: [] - bearerToken: [] /api/v1/themes/{slug}/diff: get: summary: Diff two revisions description: A token-level comparison. Only the token tree is compared; a metadata change is reported as a boolean rather than field by field. tags: - history responses: '200': description: The differences. content: application/json: schema: $ref: '#/components/schemas/VersionDiff' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: slug in: path required: true description: The theme slug. schema: type: string - name: from in: query required: true description: The base revision. schema: type: integer minimum: 1 - name: to in: query required: true description: The comparison revision. schema: type: integer minimum: 1 security: - sessionCookie: [] - bearerToken: [] /api/v1/tokens/validate: post: summary: Validate a token document description: Runs the engine's validator without persisting anything. The editor calls it as the user works, which is exactly why it must not write. tags: - tokens responses: '200': description: The findings. content: application/json: schema: $ref: '#/components/schemas/ValidationResult' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' requestBody: required: true content: application/json: schema: type: object required: - tokens properties: tokens: &id006 $ref: '#/components/schemas/TokenDocument' example: tokens: color: semantic: primary: $type: color $value: '#4f46e5' security: - sessionCookie: [] - bearerToken: [] /api/v1/tokens/contrast: get: summary: Measure WCAG contrast description: Reports the ratio and the highest conformance level it satisfies. The editor uses this to flag an unreadable combination before it is saved rather than after it is published. tags: - tokens responses: '200': description: The measurement. content: application/json: schema: $ref: '#/components/schemas/ContrastResult' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: fg in: query required: true description: The foreground colour. schema: type: string - name: bg in: query required: true description: The background colour. schema: type: string security: - sessionCookie: [] - bearerToken: [] /api/v1/tokens/palette: get: summary: Generate a colour ramp description: Produces the canonical 11-step ramp for a hue. Exposed so the editor's palette builder and the built-in themes cannot diverge from the engine's ramp curve. tags: - tokens responses: '200': description: The ramp. content: application/json: schema: $ref: '#/components/schemas/RampResponse' headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: hue in: query required: false description: Hue in degrees. schema: type: number minimum: 0 maximum: 360 default: 250 - name: saturation in: query required: false description: Saturation at the mid steps, percent. schema: type: number minimum: 0 maximum: 100 default: 78 - name: lightness in: query required: false description: Lightness at step 500, percent. schema: type: number minimum: 20 maximum: 80 default: 55 - name: hueSpread in: query required: false description: Hue drift from the lightest to the darkest step. A value of 12 reproduces the warm-to-cool lean of a modern perceptual ramp. schema: type: number minimum: 0 maximum: 40 default: 12 security: - sessionCookie: [] - bearerToken: [] /api/v1/tokens/schema: get: summary: The token schema description: The group and kind registry, so a client renders the right control per token type instead of duplicating a list that would drift. tags: - tokens responses: '200': description: The schema. content: application/json: schema: $ref: '#/components/schemas/TokenSchema' headers: Cache-Control: description: Cacheable. schema: type: string '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: - sessionCookie: [] - bearerToken: [] /api/v1/admin/users: get: summary: List known accounts description: Accounts seen at least once at login. Roles are overwritten from the provider on every login rather than merged, so a user removed from a group actually loses access. tags: - admin responses: '200': description: The users. content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/User' headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: limit in: query required: false description: '' schema: type: integer minimum: 1 maximum: 500 default: 100 security: - sessionCookie: [] - bearerToken: [] /api/v1/admin/audit: get: summary: Read the activity log description: Every mutation in the administration API. tags: - admin responses: '200': description: A page of entries. content: application/json: schema: $ref: '#/components/schemas/AuditPage' headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' parameters: - name: actor in: query required: false description: Filter by actor. schema: type: string - name: action in: query required: false description: Filter by action. schema: type: string - name: entityType in: query required: false description: '' schema: type: string - name: entityId in: query required: false description: '' schema: type: string - name: since in: query required: false description: '' schema: type: string format: date-time - name: limit in: query required: false description: '' schema: type: integer minimum: 1 maximum: 500 default: 50 - name: offset in: query required: false description: '' schema: type: integer minimum: 0 default: 0 security: - sessionCookie: [] - bearerToken: [] /api/v1/admin/settings: get: summary: Read instance settings description: The settings the engine reads at run time. Everything requiring a restart is environment configuration and belongs in the Helm chart, not in a database row. tags: - admin responses: '200': description: The settings. content: application/json: schema: type: object properties: items: type: object additionalProperties: $ref: '#/components/schemas/Setting' headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: - sessionCookie: [] - bearerToken: [] put: summary: Write instance settings description: Writing the default theme clears the render cache, because it is baked into the runtime manifest. Unrecognised keys are rejected rather than silently stored. tags: - admin responses: '200': description: The keys written. content: application/json: schema: type: object properties: updated: type: array items: type: string headers: *id002 '400': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' requestBody: required: true content: application/json: schema: type: object additionalProperties: type: string description: 'Setting keys to values. Recognised keys: `default.theme`, `default.appearance`, `runtime.public`, `runtime.apiBase`.' example: default.theme: midnight default.appearance: dark security: - sessionCookie: [] - bearerToken: [] /api/v1/admin/cache/rebuild: post: summary: Rebuild the render cache description: 'Clears and re-warms it. Useful after a bulk change, or to reclaim memory: the cache holds every published theme multiplied by appearance and variant.' tags: - admin responses: '200': description: What was warmed. content: application/json: schema: type: object properties: themes: type: integer artefacts: type: integer headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' '403': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: - sessionCookie: [] - bearerToken: [] /api/v1/stats: get: summary: Dashboard counters description: Theme, key, user and audit counts, plus the render cache hit rate. tags: - themes responses: '200': description: The counters. content: application/json: schema: $ref: '#/components/schemas/Stats' headers: *id002 '401': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: - sessionCookie: [] - bearerToken: [] /healthz: get: summary: Liveness, with build information description: Does **not** touch the database, deliberately. A liveness probe that failed on a slow database would have the orchestrator restart a process that is otherwise healthy, turning a dependency blip into an outage. tags: - operational responses: '200': description: Healthy. content: application/json: schema: $ref: '#/components/schemas/Health' '500': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /livez: get: summary: Liveness description: Answers as long as the process can serve HTTP. tags: - operational responses: '200': description: Healthy. content: application/json: schema: $ref: '#/components/schemas/Health' '500': description: Error content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] /readyz: get: summary: Readiness description: Reports whether the service can actually serve traffic, which means the database must answer within a bounded time. Warns when the database is in-memory, which is legitimate in a test and never in production. tags: - operational responses: '200': description: Ready. content: application/json: schema: $ref: '#/components/schemas/Health' headers: *id002 '503': description: Not ready. content: application/json: schema: $ref: '#/components/schemas/Health' security: [] /metrics: get: summary: Prometheus metrics description: Request counts labelled by **route pattern** rather than concrete path — a thousand distinct theme slugs must not become a thousand time series — a latency histogram, the render cache hit rate, and Go runtime gauges. Disabled when `config.enableMetrics` is false. tags: - operational responses: '200': description: The exposition text. content: text/plain: schema: type: string headers: *id002 security: [] /rt/v1/embed-page: get: summary: A shareable integration page description: 'A complete HTML page showing the snippet for a theme next to a live preview of it. Separate from /rt/v1/embed because the two have different callers: this one is a page a human opens, that one is data a UI fetches. Every code sample on the page comes from the same builders, so what is copied here is byte-for-byte what the admin UI shows.' tags: - runtime parameters: - name: slug in: query required: false description: The theme to show. schema: type: string responses: '200': description: The page. content: text/html: schema: type: string '400': description: Invalid slug. content: application/json: schema: $ref: '#/components/schemas/ApiError' '404': description: No such theme. content: application/json: schema: $ref: '#/components/schemas/ApiError' security: [] components: securitySchemes: sessionCookie: type: apiKey in: cookie name: go-bananas-session description: An HMAC-signed session cookie. Set `HttpOnly`, `Secure` and `SameSite`. Issued by the OIDC login flow. There is no server-side session table, so any replica can authenticate any request. bearerToken: type: http scheme: bearer description: An API key. May also be supplied as a `token` query parameter, because a stylesheet link cannot carry a header. Stored as a SHA-256 digest. schemas: ApiError: type: object description: 'The uniform error body. `code` is stable and part of the contract: clients branch on it, so its meaning must not change between releases.' required: - code - message properties: code: type: string description: A stable machine-readable code. examples: - validation_failed message: type: string description: A human-readable explanation. details: type: array items: $ref: '#/components/schemas/FieldError' requestId: type: string description: Correlates the response with the server log. FieldError: type: object required: - field - message properties: field: type: string description: The JSON path or dotted token path. message: type: string Token: type: object required: - $value description: A single token leaf, in the W3C Design Tokens Community Group format. properties: $type: type: string description: The token type. examples: - color - dimension - fontSize - cubicBezier $value: description: 'The value: a string, number, boolean or number array.' oneOf: - type: string - type: integer - type: boolean - type: array items: type: integer $description: type: string TokenDocument: type: object additionalProperties: true description: A design-token document. `$appearances` holds a partial document per colour scheme, deep-merged over the base, so a token absent from the dark overrides simply inherits its light value. properties: $schema: type: string $appearances: type: object additionalProperties: true ThemeStatus: type: string enum: - draft - published - archived description: Publication state. Only `published` is served to the runtime. Appearance: type: string enum: - light - dark description: Colour scheme. Theme: type: object required: - id - slug - name - status - defaultAppearance - tokens - system - version - createdAt - updatedAt properties: id: type: string slug: type: string pattern: ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$ name: type: string description: type: string status: *id005 defaultAppearance: &id007 $ref: '#/components/schemas/Appearance' tokens: *id006 meta: type: object additionalProperties: true description: Opaque to the engine apart from `tags`. system: type: boolean description: A built-in theme. Cannot be deleted. version: type: integer minimum: 1 description: Bumped on every mutation. The render cache key. createdAt: type: string format: date-time updatedAt: type: string format: date-time createdBy: type: string updatedBy: type: string ThemeSummary: type: object description: 'A theme without its token document. Listings never include them: they are the bulk of each row and a listing does not need them.' properties: id: type: string slug: type: string name: type: string description: type: string status: *id005 defaultAppearance: *id007 meta: type: object additionalProperties: true system: type: boolean version: type: integer createdAt: type: string format: date-time updatedAt: type: string format: date-time createdBy: type: string updatedBy: type: string ThemePage: type: object required: - items - total - limit - offset properties: items: type: array items: $ref: '#/components/schemas/ThemeSummary' total: type: integer limit: type: integer offset: type: integer ThemeMutation: type: object description: Create or update. Absent fields are left unchanged on update. properties: slug: type: string pattern: ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$ name: type: string description: type: string status: *id005 defaultAppearance: *id007 tokens: *id006 meta: type: object additionalProperties: true message: type: string description: Recorded in the version history. baseVersion: type: integer description: Optimistic concurrency. The write is rejected with 409 when the stored version differs, which stops two editors silently overwriting each other. ThemeVersion: type: object description: 'An immutable snapshot. History is append-only: a rollback restores a snapshot as a new version rather than rewinding, so a deployed application can never lose the revision it was built against.' required: - themeId - version - slug - name - createdAt properties: themeId: type: string version: type: integer slug: type: string name: type: string description: type: string status: *id005 defaultAppearance: *id007 tokens: *id006 message: type: string author: type: string createdAt: type: string format: date-time VersionDiff: type: object required: - fromVersion - toVersion - added - removed - changed properties: fromVersion: type: integer toVersion: type: integer added: type: array items: type: string removed: type: array items: type: string changed: type: array items: $ref: '#/components/schemas/TokenDiff' metaChanged: type: boolean TokenDiff: type: object required: - path - from - to properties: path: type: string from: type: string to: type: string ValidationIssue: type: object required: - path - severity - message properties: path: type: string severity: type: string enum: - error - warning - info message: type: string hint: type: string ValidationResult: type: object required: - valid - tokenCount - issues properties: valid: type: boolean description: False when any issue is an error. Errors block publication; warnings do not. tokenCount: type: integer issues: type: array items: $ref: '#/components/schemas/ValidationIssue' ContrastResult: type: object required: - foreground - background - ratio - level properties: foreground: type: string background: type: string ratio: type: number rounded: type: number level: type: string enum: - fail - aa-large - aa - aaa thresholds: type: object properties: aaLarge: type: number aa: type: number aaa: type: number bestForeground: type: string RampResponse: type: object description: A generated colour ramp. properties: options: type: object properties: Hue: type: number Saturation: type: number Lightness: type: number HueSpread: type: number shades: type: array items: type: string ramp: type: object additionalProperties: $ref: '#/components/schemas/Token' TokenSchema: type: object description: The engine's group and kind registry, so a client renders the right control per token type instead of duplicating a list that would drift. properties: id: type: string prefix: type: string kinds: type: array items: type: string groups: type: array items: type: object properties: name: type: string kind: type: string description: type: string recommended: type: array items: type: string shades: type: array items: type: string ImportRequest: type: object required: - slug - tokens properties: slug: type: string name: type: string tokens: *id006 meta: type: object additionalProperties: true asDraft: type: boolean default: true description: Import as a draft, so an import can never publish something by accident. overwrite: type: boolean description: Replace an existing theme. Without it, an existing slug is a 409 rather than a silent overwrite. message: type: string ThemeExport: type: object description: The portable document. Import and export are exact inverses, so migrating a theme between environments is a file copy rather than a project. properties: $schema: type: string slug: type: string name: type: string status: *id005 version: type: integer tokens: *id006 meta: type: object additionalProperties: true exportedAt: type: string format: date-time exportedBy: type: string Session: type: object description: 'The current session. Always answers 200: an absent session is the normal state on first load, not an error the UI should have to handle as one.' required: - authenticated - mode properties: authenticated: type: boolean mode: type: string enum: - oidc - dev - none identity: type: object properties: subject: type: string email: type: string name: type: string roles: type: array items: type: string enum: - viewer - editor - admin method: type: string expiresAt: type: string format: date-time permissions: type: object additionalProperties: type: boolean description: A UI convenience only. The server enforces every check, so hiding a control is never the control. preferences: $ref: '#/components/schemas/UserPreferences' description: Present once authenticated. Reported with the session so the UI can preselect a theme without a second request. devBypass: type: boolean loginUrl: type: string UserPreferences: type: object description: 'Per-account interface preferences. Both fields empty means nothing has been chosen, which is a valid state rather than a missing one: the client then applies its own default.' properties: selectedTheme: type: string description: A published theme slug, or empty for no choice. Names the theme rather than copying it, so editing or unpublishing that theme shows through everywhere. A stored value naming a theme that has since been deleted or archived reads back as empty. selectedAppearance: $ref: '#/components/schemas/Appearance' description: Which of the theme''s two colour schemes to render. Ignored, and dropped on write, when selectedTheme is empty. AppConfig: type: object description: What the UI needs before a session exists. Contains no secret material. properties: mode: type: string issuer: type: string runtimeBase: type: string publicBase: type: string basePath: type: string runtimeApiVersion: type: string variablePrefix: type: string defaultTheme: type: string User: type: object properties: subject: type: string email: type: string name: type: string roles: type: array items: type: string method: type: string firstSeen: type: string format: date-time lastLogin: type: string format: date-time AuditEntry: type: object description: One recorded action. The actor comes from the authenticated identity, never from the request body. required: - id - at - actor - action properties: id: type: integer at: type: string format: date-time actor: type: string actorEmail: type: string action: type: string enum: - theme.create - theme.update - theme.delete - theme.publish - theme.archive - theme.duplicate - theme.rollback - theme.import - key.create - key.revoke - auth.login - auth.logout entityType: type: string entityId: type: string summary: type: string detail: type: object additionalProperties: true remoteIp: type: string AuditPage: type: object required: - items - total - limit - offset properties: items: type: array items: $ref: '#/components/schemas/AuditEntry' total: type: integer limit: type: integer offset: type: integer Setting: type: object required: - key - value - updatedAt properties: key: type: string value: type: string updatedAt: type: string format: date-time updatedBy: type: string Stats: type: object properties: store: type: object properties: themes: type: integer themesByStatus: type: object additionalProperties: type: integer users: type: integer auditEntries: type: integer databaseSizeBytes: type: integer versionsRetaind: type: integer cache: type: object properties: CSSHits: type: integer CSSMisses: type: integer JSONHits: type: integer JSONMisses: type: integer Entries: type: integer ResolvedTheme: type: object description: A theme with every token resolved for one appearance, including derived tokens, because consumers should not reimplement the derivation. required: - theme - tokens - variables properties: theme: type: object properties: slug: type: string name: type: string description: type: string version: type: integer appearance: *id007 defaultAppearance: *id007 updatedAt: type: string format: date-time tokens: *id006 variables: type: object additionalProperties: type: string description: Flat CSS custom property name to rendered value. meta: type: object additionalProperties: true ManifestEntry: type: object required: - slug - name - defaultAppearance - version - css - tokens properties: slug: type: string name: type: string description: type: string defaultAppearance: *id007 version: type: integer tags: type: array items: type: string css: type: string format: uri tokens: type: string format: uri Manifest: type: object description: 'The discovery document a loader fetches once to build a theme picker. Deliberately small: no token data, so a page with a picker does not download every theme.' required: - apiVersion - baseUrl - themes properties: apiVersion: type: string default: type: string baseUrl: type: string format: uri themes: type: array items: $ref: '#/components/schemas/ManifestEntry' Health: type: object properties: status: type: string enum: - ok - ready - not ready version: type: string uptime: type: string checks: type: object additionalProperties: true EmbedSnippet: type: object description: A ready-to-paste integration snippet for one theme. required: - slug - name - version - baseUrl - language - snippet properties: slug: type: string description: 'The theme, which exists: the endpoint validates it.' name: type: string description: The theme's display name. version: type: integer description: The version the snippet was generated against. baseUrl: type: string description: The runtime base URL baked into the snippet. language: type: string enum: - html - js - tsx description: The syntax of the snippet. snippet: type: string description: The code to paste. Values interpolated into it are escaped for the language. note: type: string description: Anything else the integrator needs to know. security: - sessionCookie: [] - bearerToken: []