Skip to content

Feedback APIs

Eric Fitzgerald edited this page May 9, 2026 · 1 revision

Feedback APIs (Usability + Content)

TMI exposes two complementary feedback APIs. Usability feedback captures thumbs-up/thumbs-down on UI surfaces; any authenticated user can submit it, and only admins can read it. Content feedback captures thumbs-up/thumbs-down on AI- or automation-generated artifacts (notes, diagrams, threats, threat-classification fields) inside a specific threat model; any reader on the parent threat model can submit and read it.

Both surfaces are append-only. Feedback rows are not editable, not patchable, and not deletable through the API. Genuinely abusive content is removed by an operator via direct DB tooling — see the operator section below.

This page targets two audiences: tmi-ux developers wiring the client-side feedback UI, and TMI operators querying the persisted data for analytics. It complements the OpenAPI reference (api-schema/tmi-openapi.json) — the spec is the source of truth for the wire format; this page documents the higher-level rules that the spec doesn't capture in machine-readable form.

Endpoint summary

Endpoint Purpose Auth Authorization
POST /usability_feedback Submit a UI-surface thumbs-up/down Any authenticated user None
GET /usability_feedback List/filter all UI feedback Any authenticated user Admin role required
GET /usability_feedback/{id} Fetch a single UI feedback row Any authenticated user Admin role required
POST /threat_models/{id}/feedback Submit a content thumbs-up/down on an artifact in this TM Any authenticated user Reader+ on the parent TM
GET /threat_models/{id}/feedback List/filter content feedback for this TM Any authenticated user Reader+ on the parent TM
GET /threat_models/{id}/feedback/{feedback_id} Fetch a single content feedback row Any authenticated user Reader+ on the parent TM

Authentication is bearer-JWT (Authorization: Bearer <token>) for every endpoint. Authorization rules above are enforced by the middleware listed in the OpenAPI x-tmi-authz vendor extension.

For tmi-ux developers

Usability feedback (/usability_feedback)

Submit

POST /usability_feedback
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "sentiment": "up",
  "surface": "threat_modeling.dfd_canvas",
  "client_id": "tmi-ux",
  "client_version": "0.42.1",
  "client_build": "abc1234",
  "verbatim": "Edge routing feels much better in this build.",
  "user_agent": "Mozilla/5.0 (...) Chrome/...",
  "viewport": "1920x1080",
  "user_agent_data": { "platform": "macOS", "mobile": false }
}
Field Required Pattern / max Notes
sentiment Yes enum: up, down
surface Yes ^[a-z][a-z0-9_.-]{0,31}$, max 32 Logical UI surface — developer-supplied tag. Examples: threat_modeling.dfd_canvas, intake.survey_step_3. Don't include user-specific paths or IDs.
client_id Yes ^[a-z][a-z0-9_-]{0,31}$, max 32 Use tmi-ux for the official client.
client_version No max 32 chars Semver string.
client_build No ^[0-9a-f]{7,12}$ Short git commit hash of the client build.
user_agent No max 512 chars Browser UA header value.
viewport No ^\d{1,5}x\d{1,5}$ Viewport size as 1280x1024.
user_agent_data No JSON object, ≤ 4 KB serialized NavigatorUAData payload (or anything that serializes that way).
verbatim No max 2048 bytes Free-text comment. The byte limit is enforced server-side.

Server responses:

Status Body Meaning
201 Created UsabilityFeedback (input + id, created_by, created_at) Stored.
400 Bad Request Error Validation failure (regex, enum, missing required field).
401 Unauthorized Error Missing/invalid JWT.
413 Payload Too Large Error verbatim over 2048 bytes or user_agent_data over 4 KB.

Read (admin only)

GET /usability_feedback supports filters and pagination via query parameters. The handler enforces 1 ≤ limit ≤ 1000 and offset ≥ 0.

Param Notes
sentiment up or down
client_id Exact match (e.g. tmi-ux)
surface Exact match (e.g. threat_modeling.dfd_canvas)
created_after, created_before RFC 3339 timestamp
limit Default 50, max 1000
offset Default 0

Response shape: { "items": [UsabilityFeedback, ...], "total": <int64> }.

Content feedback (/threat_models/{threat_model_id}/feedback)

Submit

POST /threat_models/8c5f5e51-.../feedback
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "sentiment": "down",
  "target_type": "threat",
  "target_id": "9b2a4c01-...",
  "false_positive_reason": "detection_misfired",
  "false_positive_subreason": "code_does_not_exist",
  "verbatim": "This rule fired on a code path that's behind a feature flag we never enabled.",
  "client_id": "tmi-ux",
  "client_version": "0.42.1"
}
Field Required Notes
sentiment Yes up or down.
target_type Yes note, diagram, threat, or threat_classification.
target_id Yes UUID of the targeted artifact. For threat_classification, this is the threat ID (the target_field then names the classification field). The target row must belong to the parent threat model.
target_field Conditional Required when target_type=threat_classification; forbidden otherwise. Max 64 chars. Examples: cwe, severity, mitigation.
false_positive_reason No Allowed only when sentiment=down AND target_type=threat. See the taxonomy below.
false_positive_subreason No Allowed only when false_positive_reason is a reason that has subreasons. Must be a valid subreason for the chosen reason.
verbatim No Free text, max 2048 bytes. The UX rule is to require verbatim when the user picks any "wrong" reason — see below.
client_id Yes Pattern as above.
client_version No Max 32 chars.
False-positive reason ↔ subreason taxonomy

The server enforces this map (api/content_feedback_handlers.go):

Reason Allowed subreasons
detection_misfired code_does_not_exist, trigger_conditions_not_met
out_of_scope component_outside_threat_model
intended_behavior sanctioned_by_design
detection_rule_flawed not_a_real_risk, needs_tuning
real_but_mitigated (none)
real_but_not_exploitable (none)
duplicate (none)
already_remediated (none)

Sending a false_positive_subreason for a reason in the "(none)" rows returns 400 Bad Request. So does sending a subreason that doesn't match the chosen reason.

When to require verbatim

The UX rule (not enforced by the server, but expected from clients): when the user selects a reason that suggests the detection is wrong (detection_misfired, detection_rule_flawed, out_of_scope), prompt for verbatim so the analytics pipeline can see why the rule misbehaved. For "right but mitigated/duplicate/etc." reasons, verbatim is optional.

auto_generated and the AI-content variant

Notes, diagrams, and threats carry a server-managed boolean field auto_generated (read-only, sticky from creation). It is true when the row was created by an automation or service-account principal — Timmy, an addon, a webhook, or a machine token. The UI variant for content feedback (the "Was this AI suggestion useful?" prompt) should appear only on rows with auto_generated=true. For human-authored rows, content feedback is still allowed via the API, but the AI-content prompt is conceptually wrong and should not be shown.

Read

GET /threat_models/{threat_model_id}/feedback supports the following filters and 1 ≤ limit ≤ 100, offset ≥ 0:

Param Notes
target_type Exact match
target_id Exact UUID match
sentiment up or down
false_positive_reason Exact match
limit Default 20, max 100
offset Default 0

Response shape: { "items": [ContentFeedback, ...], "total": <int64> }.

GET /threat_models/{tm_id}/feedback/{feedback_id} returns a single row. If the feedback row's threat_model_id doesn't match the path's TM, the response is 404 Not Found (not 403) — the server intentionally does not leak existence across threat models.

Concurrency note (TOCTOU fix in #376)

POST /threat_models/{id}/feedback runs the target-existence check and the INSERT inside a single DB transaction with SELECT … FOR UPDATE on the target row. If the target is deleted concurrently, the API returns 400 with target_id not found in this threat model — clients should treat that as the artifact having gone away and refresh their list.

For operators

Where the data lives

Two tables in the application schema (PostgreSQL or Oracle ADB):

  • usability_feedback
  • content_feedback

The schema is GORM-managed. Column types and indexes are defined on models.UsabilityFeedback and models.ContentFeedback in api/models/models.go.

Table Notable columns
usability_feedback id (uuid), sentiment, surface, client_id, client_version, client_build, user_agent, user_agent_data (JSON), viewport, verbatim, created_by (user UUID), created_at. Indexed on sentiment, surface, created_by, created_at.
content_feedback id (uuid), threat_model_id, target_type, target_id, target_field, sentiment, verbatim, false_positive_reason, false_positive_subreason, client_id, client_version, created_by, created_at. Composite index on (threat_model_id, target_type, target_id); indexes on sentiment, false_positive_reason, created_at.

Sample analytics queries

Top-level sentiment trend by surface, last 30 days:

SELECT
  surface,
  sentiment,
  COUNT(*) AS n
FROM usability_feedback
WHERE created_at > now() - interval '30 days'
GROUP BY surface, sentiment
ORDER BY surface, sentiment;

False-positive reasons across all threat models:

SELECT
  false_positive_reason,
  false_positive_subreason,
  COUNT(*) AS n
FROM content_feedback
WHERE sentiment = 'down'
  AND target_type = 'threat'
  AND false_positive_reason IS NOT NULL
GROUP BY false_positive_reason, false_positive_subreason
ORDER BY n DESC;

Threats with the most "thumbs down" feedback (drilling into a particular TM):

SELECT
  target_id,
  COUNT(*) AS down_votes
FROM content_feedback
WHERE threat_model_id = '8c5f5e51-...'
  AND target_type = 'threat'
  AND sentiment = 'down'
GROUP BY target_id
ORDER BY down_votes DESC
LIMIT 25;

Verbatim comments tagged with a specific reason (review by a human moderator):

SELECT
  id, created_at, verbatim
FROM content_feedback
WHERE false_positive_reason = 'detection_misfired'
  AND verbatim IS NOT NULL
ORDER BY created_at DESC
LIMIT 100;

Oracle ADB note: identifiers are case-insensitive; usability_feedback resolves to USABILITY_FEEDBACK. now() becomes SYSTIMESTAMP; interval '30 days' becomes INTERVAL '30' DAY.

Retention

Feedback rows are kept indefinitely by default. There is no built-in retention policy.

  • content_feedback rows are removed automatically when the parent threat model is deleted, via the explicit cleanup in deleteThreatModelChildren (see auth/repository/deletion_repository.go). Existing Oracle ADB databases also still have a DB-level ON DELETE CASCADE foreign key from a prior schema version; the explicit cleanup runs first, so the cascade is a no-op in practice. See Oracle FK cleanup runbook for the optional one-shot DDL that drops the redundant FK.
  • usability_feedback rows are not tied to a threat model and are never automatically removed. If retention matters in your environment, schedule a periodic DELETE FROM usability_feedback WHERE created_at < now() - interval '<N> days' job.

Removing abusive content

The API is append-only — there is no DELETE endpoint for feedback rows. To remove a specific abusive row (e.g., a verbatim comment that contains slurs or PII), connect to the database directly and:

DELETE FROM content_feedback WHERE id = '<feedback-uuid>';
-- or
DELETE FROM usability_feedback WHERE id = '<feedback-uuid>';

Capture an audit note out-of-band (incident tracker, ops journal) — the application does not log feedback deletions. If you only want to redact the verbatim text and keep the row for analytics, UPDATE … SET verbatim = NULL WHERE id = ….

Immutability

There is intentionally no PATCH/PUT on feedback rows. A user who wants to "change their mind" submits a new row; analytics queries that need a per-user-per-target dedup should GROUP BY (created_by, target_id) and pick the most recent.

Cross-references

  • OpenAPI specification: api-schema/tmi-openapi.json — schemas UsabilityFeedbackInput, UsabilityFeedback, ContentFeedbackInput, ContentFeedback.
  • Source of truth (server):
    • api/usability_feedback_handlers.go
    • api/content_feedback_handlers.go
    • api/models/models.go (UsabilityFeedback, ContentFeedback)
  • Originating issues: #361 (usability feedback), #362 (content feedback), #376 (TOCTOU fix on POST), #378 (cascade-cleanup alignment).

Home

Releases


Getting Started

Deployment

Operation

Troubleshooting

Development

Integrations

Tools

API Reference

Reference

Clone this wiki locally