-
Notifications
You must be signed in to change notification settings - Fork 1
Feedback APIs
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 | 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.
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. |
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> }.
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. |
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.
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.
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.
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.
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.
Two tables in the application schema (PostgreSQL or Oracle ADB):
usability_feedbackcontent_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. |
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.
Feedback rows are kept indefinitely by default. There is no built-in retention policy.
-
content_feedbackrows are removed automatically when the parent threat model is deleted, via the explicit cleanup indeleteThreatModelChildren(seeauth/repository/deletion_repository.go). Existing Oracle ADB databases also still have a DB-levelON DELETE CASCADEforeign 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_feedbackrows are not tied to a threat model and are never automatically removed. If retention matters in your environment, schedule a periodicDELETE FROM usability_feedback WHERE created_at < now() - interval '<N> days'job.
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 = ….
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.
- OpenAPI specification:
api-schema/tmi-openapi.json— schemasUsabilityFeedbackInput,UsabilityFeedback,ContentFeedbackInput,ContentFeedback. - Source of truth (server):
api/usability_feedback_handlers.goapi/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).
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Identity Linking
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Bootstrapping Production
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Configuring Local Development
- Managing Operational Settings
- Content Extractors - Limits and Overrides
- Database Operations
- Database Security Strategies
- Transaction Isolation
- Oracle Content Feedback FK Cleanup
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Local Development Cluster
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions
- Issue Tracker Integration
- Webhook Integration
- Addon System
- MCP Integration
- Delegated Content Providers
- Setting Up Google Content Providers
- API Clients
- API Client Maintenance
- Database Tool Reference
- TMI Terraform Analyzer
- TMI Promtail Logger
- WebSocket Test Harness