feat: add device custom fields#6248
Conversation
Add `CustomFields map[string]string` to the Device model (BSON/JSON tagged) and a `*map[string]string` pointer field to DeviceUpdate so that omitting the field in a PUT payload leaves existing values untouched.
- Service: apply CustomFields from request when non-nil; register
"custom_fields" as a valid filter field with "contains" operator
- MongoDB: route custom_fields filter via ParseCustomFieldsFilter using
\$objectToArray + \$regexMatch to search across all map values
- PostgreSQL entity: add JSONB column mapping to Device entity
- PostgreSQL filters: add fromCustomFieldsFilter that generates an
EXISTS (jsonb_each_text) subquery for ILIKE value matching
- Migration 002: add custom_fields jsonb column with default '{}'
golangci-lint uses -fuse-ld=gold which requires binutils-gold on Alpine ARM64. Without it the build fails with "cannot find 'ld'".
- Device list: new Custom Fields column showing key/value badges with pill styling; search also queries custom field values - Device detail: CustomFieldsSection component with add form, inline delete confirmation, and useUpdateDeviceCustomFields mutation - useDevices: extend buildFilter to OR-search across custom_fields values - useDeviceMutations: export useUpdateDeviceCustomFields hook
- pg/internal: TestFromCustomFieldsFilter and TestParseFilterProperty_CustomFields - mongo/internal: TestParseCustomFieldsFilter covering contains, unsupported operators, and non-string value error - services: extend TestDeviceUpdate with cases for setting, clearing, and nil (no-op) custom fields
…fields - Devices list: rendering, loading, empty state, row navigation, custom fields column (empty, single field, multiple fields), error state - DeviceDetails: loading spinner, device data rendering, custom fields section display, add form, delete confirmation flow, mutation calls, duplicate key validation
The search filter now ORs across both name and custom_fields. Update existing test expectations accordingly.
d361b29 to
8473bdf
Compare
|
/review |
|
I'll analyze this and get back to you. |
|
/review |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @gustavosbarreto's task in 5m 29s —— View job Code Review CompleteReviewed 30 files across code quality, security, testing, language patterns, and architecture. 3 inline issues posted (1 critical, 1 high, 1 medium).
Additional notes
If you've addressed the feedback and want a new review, tag |
- pg entity: add nullzero,default:'{}' to CustomFields bun tag so that
nil maps are omitted from INSERT (letting the column DEFAULT apply)
instead of sending SQL NULL and violating the NOT NULL constraint
- model: remove omitempty from bson tag on CustomFields so that an
empty map is included in MongoDB \$set operations, allowing callers
to clear all fields by passing an empty object
- request: add validation limits to CustomFields (max 20 entries, keys
1-64 chars, values max 256 chars) to prevent storage abuse
|
/review |
|
Claude finished @otavio's task in 9m 34s —— View job Code Review CompleteReviewed 30 files across code quality, security, testing, language patterns, and architecture — no new issues found. Previous review issues — all addressedThe 3 issues from the prior review have been fixed in commit
Verification notes
If you've addressed the feedback and want a new review, tag |
|
When you have some free time, please consider doing another review. |
|
/review |
|
Claude finished @gustavosbarreto's task in 13m 12s —— View job Code Review CompleteReviewed 30 files across code quality, security, testing, language patterns, and architecture. 2 inline issues posted (2 high).
Previous review issues — all addressedThe 3 issues from the prior review remain fixed in commit
Verification notes
If you've addressed the feedback and want a new review, tag |
- Remove name prop from CustomFieldsSection (was causing silent device
rename when custom fields were mutated due to stale closure value)
- Send name: "" in mutation body so only custom_fields are changed
- Guard remove buttons and add form behind useHasPermission("device:rename")
|
/review |
|
Claude finished @gustavosbarreto's task in 1m 22s —— View job Code Review CompleteReviewed 30 files across code quality, security, testing, language patterns, and architecture — no new issues found. Previous review issues — all addressedThe 5 issues from prior reviews have been fixed in commits
Verification notes
If you've addressed the feedback and want a new review, tag |
|
🎉 looks like the PR is good now Any chance to get it released in earlier future? |
|
Thanks! It'll go out in the next release. |

Motivation
As device fleets grow, operators often need to attach context-specific metadata to individual devices — things like physical location, environment tags, asset identifiers, or team ownership. Without a dedicated mechanism, this information ends up scattered across external spreadsheets or encoded into hostnames, making it hard to query and maintain.
This feature introduces custom fields: a simple key-value map attached to each device. Operators can freely define any metadata they need, and that metadata becomes a first-class citizen in the dashboard — visible in the device list, searchable alongside the hostname, and fully manageable from the device detail page without touching the API directly.
Screen.Recording.2026-04-28.at.8.17.06.AM.mov
Summary
custom_fields map[string]stringto the Device model, stored as JSONB in PostgreSQL and an embedded document in MongoDBPUT /devices/{uid}endpoint — nil means no change, empty map clears all fieldsChanges
pkgCustomFieldstoDevicemodel andDeviceUpdaterequest DTOapi$objectToArrayfilter, PostgreSQL JSONB filter, migration 002openapicustom_fieldsto device schema and PUT request bodydockerbinutils-goldto Alpine images for ARM64 linker support (see note below)ui(Vue)ui-reacttest(api)test(ui-react)Note: binutils-gold in Alpine Dockerfiles
During local development on an ARM64 machine (Apple Silicon), the Docker builds were failing with:
The root cause is that
golangci-lintis compiled with-fuse-ld=gold, which requires thegoldlinker (binutils-goldpackage) to be present on the system. Alpine's defaultbinutilsdoes not include it. Addingbinutils-goldto the Alpine images that installgolangci-lintresolves the build failure on ARM64 hosts.Test plan
go test ./api/services/... ./api/store/pg/internal/... ./api/store/mongo/internal/...cd ui-react/apps/console && npx vitest run src/pages/devices/__tests__PUT /devices/{uid}with{"name": "x", "custom_fields": {"env": "prod"}}→ 200GET /devices/{uid}→ response includescustom_fields/devicesby custom field value returns matching devices