Skip to content

Provisioning Domain

Overview

The Provisioning Domain handles device lifecycle management for Digital Signage displays (LG WebOS, Android, etc.). It manages device pairing via OTP codes, real-time status monitoring via heartbeats, remote playback control via MQTT, and IP-based geolocation enrichment.


Architecture

┌───────────────────────────────────────────────────────────────┐
│                    Provisioning Domain                        │
│                                                               │
│  Physical Screen                 Platform                     │
│  ┌─────────────┐                                              │
│  │   Display   │──── 1. generate-code ──▶ provisioning_codes  │
│  │  (WebOS /   │◀─── shows 6-digit OTP                       │
│  │   Android)  │                                              │
│  │             │──── 2. claim-code ──────▶ devices (created)  │
│  │             │◀─── deviceToken (JWT)                        │
│  │             │                                              │
│  │             │──── 3. heartbeat-public ▶ devices.last_seen_at│
│  │             │                                              │
│  │             │◀─── MQTT: play ─────────  /devices/:id/play  │
│  │             │◀─── MQTT: STOP ──────────  /devices/:id/stop │
│  └─────────────┘                                              │
│                                                               │
│  Device status = 'Online' if last_seen_at > NOW() - 5 min    │
└───────────────────────────────────────────────────────────────┘

MQTT Topics:

Topic Trigger Payload
tellyboard/devices/{deviceId}/play POST /devices/:id/play Playlist JSON with items/zones
tellyboard/devices/{deviceId}/player/command POST /devices/:id/stop "STOP" string

Request Headers:

Header Description
X-Organization-ID Required on all management endpoints
X-Workspace-ID Optional — filters device list/get to workspace scope

Data Model

Device

The core entity representing a registered physical display.

Field Type Notes
id string UUID
organizationId string Tenant scope
workspaceId *string Optional workspace scope
name string Display name
serialNumber string Hardware serial; set to SN-{code} on claim
status string Derived: Online if last_seen_at > NOW() - 5 min, else Offline
lastSeenAt *time.Time Updated by heartbeat
currentPlaylistId *string Last playlist pushed to device; cleared on stop
locationId *string Links to Spatial domain locations.id (space type)
volume int Default 100 on claim
orientation string Default landscape on claim
isRemoteControlEnabled bool Default true on claim
isPanelControlEnabled bool Default true on claim
wolEnabled / dpmEnabled bool Wake-on-LAN / Display Power Management
ipAddress / macAddress string Network identifiers
manufacturer / model / playerVersion / firmwareVersion / osVersion *string Hardware metadata
storageUsedBytes / storageTotalBytes *int64 Storage stats
continent, country, city, isp string Geo fields, populated by /geo endpoint

ClaimCodePayload

Request body for /devices/claim-code.

Field Type Notes
code string 6-digit OTP shown on physical screen
name string Friendly name to assign the new device

provisioning_codes (DB table, no Go model)

Tracks OTP sessions.

Column Notes
id UUID — used as provisioningId for polling
code 6-digit numeric OTP
expires_at 10 minutes from generation
device_id NULL until claimed; set on claim/relink
organization_id Set on claim

Routes

Method Path Handler Description
GET /devices HandleGetDevices List all devices for org, optional workspace filter
POST /devices HandleCreateDevice Create device manually (name, description, serialNumber)
GET /devices/:deviceId HandleGetDeviceByID Get single device
PUT /devices/:deviceId HandleUpdateDevice Update device settings and metadata
DELETE /devices/:deviceId HandleDeleteDevice Delete device; nullifies linked provisioning codes
POST /devices/generate-code HandleGenerateDeviceCode Generate a 6-digit OTP pairing code (expires in 10 min)
POST /devices/claim-code HandleClaimDeviceCode Claim OTP → creates device, links provisioning code (transactional)
POST /devices/relink HandleRelinkDevice Relink an existing device to a new OTP code
GET /devices/check-claim/:provisioningId HandleCheckClaimStatus Poll claim status: pending / claimed / expired
POST /devices/heartbeat-public HandleDeviceHeartbeatPublic Update last_seen_at (no auth required — called by device)
POST /devices/:deviceId/play HandleSendPlaylist Normalize and push playlist via MQTT; updates current_playlist_id
POST /devices/:deviceId/stop HandleStopPlayback Send MQTT STOP command; clears current_playlist_id
POST /devices/:deviceId/geo HandleGetGeoInfo Lookup geo via ip-api.com using device IP; updates geo fields
PATCH /devices/:deviceId/location HandleAssignDeviceLocation Assign device to a spatial location (space)

Key Creation Flows

1. Device Pairing (OTP Flow)

# Step 1: Screen requests a pairing code (called from the display hardware)
POST /devices/generate-code
  → Generates 6-digit numeric OTP
  → Stores in provisioning_codes with 10-minute TTL
  → Returns: { "provisioningId": "uuid", "code": "483921", "expiresAt": "..." }

# Step 2: Operator enters the code on the web platform
POST /devices/claim-code
  Headers: X-Organization-ID
  Body: { "code": "483921", "name": "Lobby Screen A" }
  → Validates code is not expired and not yet claimed
  → Creates device (serialNumber = "SN-483921", volume=100, orientation=landscape)
  → Links provisioning_codes.device_id = new device id
  → Returns: { "id": "device-uuid", "name": "Lobby Screen A", "status": "Online" }

# Step 3: Screen polls until claimed
GET /devices/check-claim/:provisioningId
  → Returns { "status": "pending" }  (while waiting)
  → Returns { "status": "claimed", "deviceId": "...", "deviceToken": "JWT" }
  → Returns { "status": "expired" }  (after 10 min)

2. Heartbeat (Device → Platform)

POST /devices/heartbeat-public
  Body: { "deviceId": "device-uuid" }
  → No auth required (called directly from device)
  → Updates devices.last_seen_at = NOW()
  → Device is considered 'Online' for 5 minutes after last heartbeat

3. Push Playlist to Device

POST /devices/:deviceId/play
  Headers: X-Organization-ID
  Body: {
    "id": "playlist-uuid",
    "playlistName": "Morning Show",
    "items": [
      { "mediaId": "...", "name": "...", "mediaType": "video", "duration": 30, "url": "...", "storageKey": "..." }
    ]
  }
  → Normalizes items (supports both "id"/"mediaId" and "type"/"mediaType" field names)
  → Also supports "zones": [{ "items": [...] }] for multi-zone layouts
  → Updates devices.current_playlist_id
  → Publishes to MQTT topic: tellyboard/devices/{deviceId}/play
  → Returns: { "success": true, "message": "Playlist sent and saved" }

POST /devices/:deviceId/stop
  → Clears current_playlist_id
  → Publishes "STOP" to MQTT topic: tellyboard/devices/{deviceId}/player/command
  → Returns: { "status": "stopped" }

4. Geolocation Enrichment

POST /devices/:deviceId/geo
  Headers: X-Organization-ID
  → Uses device's stored ip_address, or falls back to request IP / X-Forwarded-For
  → Private IPs (127.x, 192.168.x, 10.x) query ip-api.com without IP (uses server IP)
  → Calls: http://ip-api.com/json/{ip}
  → Updates: continent, country, countryCode, regionName, city, isp, ip_address
  → Returns geo fields on success

POST /devices/relink
  Headers: X-Organization-ID
  Body: { "code": "new-otp", "deviceId": "existing-device-uuid" }
  → Validates OTP is valid and unclaimed
  → Validates device belongs to org
  → Links provisioning_codes.device_id = existing device
  → Sets devices.status = 'Online', last_seen_at = NOW()
  → Use case: device was factory reset or swapped hardware

Changelog

Date Author Change
2026-06-17 Added description, architecture, data model, creation flows
2026-06-16 Pin Created the openapi