Product Metadata
Overview
The Product Metadata feature allows administrators to define custom metadata structures for IoT products and their associated devices. This flexible system enables product-specific data collection without requiring schema changes.
Key Concept:
- Products define the metadata structure (keys and data types)
- Devices provide values for these predefined fields
This two-level approach ensures data consistency across all devices of the same product type while maintaining flexibility for different product categories.
Key Features
- Template-based metadata: Define metadata structure once at the product level
- Type safety: Support for string, number, boolean, and date types
- Automatic validation: Frontend and backend validation based on defined types
- Auto-cleanup: Obsolete metadata fields are automatically removed from devices when the product template changes
- UI-driven configuration: No code changes required to add new metadata fields
- Role-based access: Only superadmins can define product metadata structures
Architecture
High-Level Flow
┌─────────────────────────────────────────────────────────────┐
│ Product Level │
│ (Define Structure: keys + types) │
│ │
│ Product.metadata = { │
│ fields: [ │
│ { key: "temperature", type: "number" }, │
│ { key: "location", type: "string" }, │
│ { key: "isActive", type: "boolean" } │
│ ] │
│ } │
└─────────────────────────────────────────────────────────────┘
│
│ Devices inherit structure
▼
┌─────────────────────────────────────────────────────────────┐
│ Device Level │
│ (Provide Values) │
│ │
│ Device.metadata = [ │
│ { key: "temperature", value: "25.5" }, │
│ { key: "location", value: "Building A" }, │
│ { key: "isActive", value: "true" } │
│ ] │
└─────────────────────────────────────────────────────────────┘
Component Architecture
Backend Components:
ProductController- Manages product metadata structure via PUT/product/:idDeviceController- Manages device metadata values via PUT/device/:id/metadataDeviceService.updateMetadata()- Business logic for metadata updates with auto-cleanup
Frontend Components:
ProductMetadataForm- Form to define product metadata structureDeviceMetadataSheet- Drawer component containing device metadata formDeviceMetadataForm- Form to edit device metadata values
Data Model
Product Metadata Structure
Stored in Product.metadata field as JSON:
interface ProductMetadata {
fields: Array<{
key: string; // Field identifier (e.g., "temperature")
type: string; // Data type: "string" | "number" | "boolean" | "date"
}>;
}
Example:
{
"fields": [
{ "key": "temperature", "type": "number" },
{ "key": "location", "type": "string" },
{ "key": "isActive", "type": "boolean" },
{ "key": "lastMaintenance", "type": "date" }
]
}
Device Metadata Values
Stored in Device.metadata field as JSON array:
interface DeviceMetadata {
key: string; // Must match a key from Product.metadata.fields
value: string; // All values stored as strings
}
Example:
[
{ "key": "temperature", "value": "25.5" },
{ "key": "location", "value": "Building A, Room 101" },
{ "key": "isActive", "value": "true" },
{ "key": "lastMaintenance", "value": "2025-11-01" }
]
Important: All values are stored as strings in the database. Type conversion happens at the UI level.
API Endpoints
Product Metadata
Update Product (including metadata)
PUT /product/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"useDigitalTwin": true,
"metadata": {
"fields": [
{ "key": "temperature", "type": "number" },
{ "key": "location", "type": "string" }
]
}
}
Response: 200 OK with updated ProductDTO
Access: Requires superadmin role
Device Metadata
Get Device (includes metadata)
GET /device/:id
Authorization: Bearer <token>
Response: 200 OK with DeviceDTO including metadata array
Note: Device metadata is included in the main device object. No separate endpoint is needed.
Update Device Metadata
PUT /device/:id/metadata
Authorization: Bearer <token>
Content-Type: application/json
[
{ "key": "temperature", "value": "25.5" },
{ "key": "location", "value": "Building A" }
]
Response: 200 OK with updated device metadata array
Behavior:
- Validates that all keys exist in the product's metadata structure
- Automatically removes obsolete fields (keys not in product template)
- Creates new
DeviceMetadatarecords for new keys - Updates existing records for matching keys
Access: Requires authenticated user with device access
Frontend Components
ProductMetadataForm
Location: apps/backoffice-frontend/src/app/[lang]/components/product/form-metadata.tsx
Purpose: Define the metadata structure for a product
Features:
- Add/remove metadata fields
- Set field key and type (string, number, boolean, date)
- Real-time validation
- Grid layout for clean UI
Usage:
import { ProductMetadataForm } from "@/app/[lang]/components/product/form-metadata";
<ProductMetadataForm product={product} />
Access: Only visible to superadmins (wrapped in AuthorizedBlock)
DeviceMetadataSheet
Location: apps/backoffice-frontend/src/app/[lang]/components/device/device-metadata-sheet.tsx
Purpose: Wrapper component that displays device metadata form in a drawer
Features:
- Opens in a right-side drawer
- Displays device description in title
- Closes automatically on successful save
Usage:
import { DeviceMetadataSheet } from "@/app/[lang]/components/device/device-metadata-sheet";
<DeviceMetadataSheet device={device} roles={session?.roles} />
DeviceMetadataForm
Location: apps/backoffice-frontend/src/app/[lang]/components/device/form/form-update-device-metadata.tsx
Purpose: Edit metadata values for a device based on product template
Features:
- Dynamically generates form fields based on product metadata structure
- Type-appropriate inputs (text, number, checkbox, date picker)
- Validation based on field type
- Success callback for drawer integration
- Uses initial metadata from device object (no separate API call)
Props:
interface Props {
device: DeviceDTO;
roles: string[];
onSuccess?: () => void;
initialMetadata?: Array<{ key: string; value: string }>;
}
Technical Flow
1. Product Configuration Flow
User (superadmin) → Product Settings Page
→ Add metadata field (key: "temperature", type: "number")
→ Save
→ PUT /product/:id with metadata structure
→ Product.metadata updated in database
2. Device Value Input Flow
User → Device Settings Page
→ Click "Manage Metadata" button
→ Drawer opens with DeviceMetadataForm
→ Form fields generated from Product.metadata.fields
→ User enters values
→ Save
→ PUT /device/:id/metadata with values array
→ DeviceService.updateMetadata() validates and saves
→ Auto-cleanup removes obsolete fields
→ Drawer closes on success
3. Auto-Cleanup Logic
When updating device metadata:
// In DeviceService.updateMetadata()
1. Fetch product metadata structure
2. Extract valid keys: ["temperature", "location", "isActive"]
3. Filter incoming metadata to only include valid keys
4. Delete existing DeviceMetadata records for this device
5. Create new records with filtered data
6. Return updated metadata array
Example:
- Product template has:
["temperature", "location"] - Device previously had:
["temperature", "location", "oldField"] - User updates with:
["temperature", "location", "newField"] - Result: Device now has
["temperature", "location"](both "oldField" and invalid "newField" removed)
Development
Prerequisites
- Node.js 18+
- Product with defined metadata structure
- Superadmin role for product configuration
- Authenticated user for device metadata editing
Local Development
No special setup required. The feature is integrated into:
- Product settings page:
/dashboard/product/[productId]/settings - Device settings page:
/dashboard/device/[deviceId]/settings
Testing
Unit Tests: Test metadata validation logic in DeviceService
Integration Tests:
- Create product with metadata structure
- Create device for that product
- Update device metadata
- Verify auto-cleanup removes obsolete fields
E2E Tests:
- Navigate to product settings as superadmin
- Add metadata fields
- Navigate to device settings
- Open metadata drawer
- Enter values and save
- Verify values persist
Configuration
Environment Variables
No specific environment variables required. Uses existing backend configuration.
Database Schema
The feature uses existing Product and Device tables with JSON fields:
model Product {
id String @id
metadata Json? // Stores metadata structure
// ... other fields
}
model Device {
id String @id
productId String
metadata Json? // Stores metadata values
product Product @relation(fields: [productId], references: [id])
// ... other fields
}
Type Definitions
Supported metadata types:
string- Text inputnumber- Number input with validationboolean- Checkboxdate- Date picker
Type mapping is handled in:
- Backend: DTO validation
- Frontend: Form field generation (
getInputForType()function)
Supported Types
| Type | UI Component | Validation | Example Value |
|---|---|---|---|
string | Text input | Required, min 1 char | "Building A" |
number | Number input | Must be valid number | "25.5" |
boolean | Checkbox | true/false only | "true" |
date | Date picker | Valid date format | "2025-11-06" |
Best Practices
Key Naming: Use descriptive, lowercase keys with no spaces (e.g., "lastMaintenance", not "Last Maintenance")
Type Selection: Choose the most specific type for your data (use
numberfor numeric values, notstring)Required Fields: All metadata fields are required. If optional fields are needed, handle empty strings in your application logic
Cleanup Awareness: Removing a field from product metadata will delete that field from ALL devices of that product
Type Consistency: Don't change field types after devices have data. Create a new field instead
Troubleshooting
Issue: "Field not found in product metadata"
Cause: Device metadata contains a key not present in product metadata structure
Solution: Either add the key to product metadata or remove it from device metadata
Issue: 400 Bad Request when saving product metadata
Cause: Missing required useDigitalTwin field in request
Solution: Ensure useDigitalTwin is included in update payload (handled automatically by ProductMetadataForm)
Issue: Metadata values disappear after saving
Cause: The keys were not present in product metadata template (auto-cleanup removed them)
Solution: First add the keys to product metadata structure, then add values to device
Issue: Cannot save boolean value
Cause: Checkbox state not properly converting to string
Solution: Ensure form handles boolean to string conversion (value should be "true" or "false" as string)
Remarks
- All metadata values are stored as strings in the database, regardless of type
- Type conversion happens at the UI layer when rendering form inputs
- Product metadata structure changes affect all devices of that product
- Superadmin role required for product metadata configuration
- No migration path for existing metadata when changing structure (fields are deleted)
- Maximum metadata fields: No hard limit, but consider UI usability (recommended max ~20 fields)
Related Documentation
- Backoffice Frontend - Main frontend application
- Backoffice Overview - Backend & Frontend overview
- User Roles - Role-based access control
- Product Metadata User Guide - Step-by-step user guide