Catalogue Domain
Manages all content and scheduling for both the CMS side (playlists, media channels, layout templates, time-blocks) and the ticketing side (ticket groups, variants, tags, banners, rail menus). The two subsystems share the same Go domain package but are registered through separate route groups.
Handles:
- Playlists — CRUD + media item management + reorder
- Media Channels (Feeds) — CRUD + zone assignments + space assignments + time-blocks + fallback config
- Layout Templates — read + filter by orientation/delivery profile
- Ticket Groups — multilingual event catalogue with gallery, variants, details
- Tags — tagging ticket groups
- Banners — promotional banner carousel management
- Rail Menus — content rails for the customer app
- VOD playlists (planned)
- Ticket group search / filtering (planned)
Architecture
graph LR
CMS["CMS Frontend (TellyBoard)"]
CustomerApp["Customer App"]
CoreAPI["CoreAPI"]
CMSDB["CMS DB\n(playlists, media_channels,\nlayout_templates, time_blocks)"]
TicketingDB["Ticketing DB\n(ticket_group, banner, rail_menu, tag)"]
CMS -->|/api/v1/playlists\n/api/v1/media-channels| CoreAPI
CMS -->|/api/ticketGroups\n/api/railMenus\n/api/banners| CoreAPI
CustomerApp -->|/api/ticketGroups\n/api/railMenus/customer| CoreAPI
CoreAPI --> CMSDB
CoreAPI --> TicketingDB
Data Model
erDiagram
Playlist {
string id
string organizationId
string workspaceId
string name
int version
int totalDuration
string status
int mediaCount
}
PlaylistItem {
string id
string playlistId
string mediaId
string itemType
int sortOrder
int durationSeconds
int zoneIndex
}
MediaChannel {
string id
string organizationId
string name
string orientation
string deliveryMode
string approvalStatus
string status
}
LayoutTemplate {
string id
string name
string orientation
bool isSystem
}
LayoutZone {
string id
string templateId
string name
float widthPercent
float heightPercent
}
TimeBlock {
string id
string channelId
string playlistId
string blockType
string startDate
string endDate
string startTime
string endTime
string recurrence
int priority
}
ChannelFallbackConfig {
string channelId
string standbySlate
string adSlate
string blackoutSlate
string defaultPlaylistId
}
TicketGroup {
uint ticketGroupId
string groupNameBm
string groupNameEn
string groupNameCn
string groupType
bool isActive
string activeStartDate
string activeEndDate
}
TicketVariant {
uint ticketVariantId
uint ticketGroupId
string nameEn
float unitPrice
}
Tag {
uint tagId
string tagName
string tagDesc
}
Banner {
uint bannerId
int placement
bool isActive
string activeStartDate
string activeEndDate
}
RailMenu {
uint railMenuId
string nameEn
int placement
bool isActive
}
Playlist ||--o{ PlaylistItem : "contains"
MediaChannel ||--o{ TimeBlock : "scheduled by"
LayoutTemplate ||--o{ LayoutZone : "has zones"
TicketGroup ||--o{ TicketVariant : "has variants"
TicketGroup }o--o{ Tag : "tagged with"
RailMenu }o--o{ TicketGroup : "contains"
CMS Endpoints (/api/v1)
Headers: X-Organization-ID, optionally X-Workspace-ID, X-User-ID.
Playlists
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/playlists |
List playlists for org |
POST |
/api/v1/playlists |
Create playlist |
GET |
/api/v1/playlists/:playlistId |
Get playlist by ID |
PUT |
/api/v1/playlists/:playlistId |
Rename playlist |
DELETE |
/api/v1/playlists/:playlistId |
Delete playlist |
POST |
/api/v1/playlists/:playlistId/media |
Replace all media items (full sync) |
GET |
/api/v1/playlists/:playlistId/media |
List playlist media items |
DELETE |
/api/v1/playlists/:playlistId/media/:mediaId |
Remove a single media item |
PUT |
/api/v1/playlists/:playlistId/items/reorder |
Reorder items by new sort_order |
Media sync is replace-all
POST /playlists/:id/media deletes all existing items and inserts the new list. There is no partial add — always send the full ordered list.
Media Channels
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/media-channels |
List all channels for org |
POST |
/api/v1/media-channels |
Create channel |
GET |
/api/v1/media-channels/:channelId |
Get channel by ID |
PUT |
/api/v1/media-channels/:channelId |
Partial update (dynamic fields) |
DELETE |
/api/v1/media-channels/:channelId |
Delete channel |
GET |
/api/v1/media-channels/:channelId/zones |
List zone assignments |
POST |
/api/v1/media-channels/:channelId/zones |
Set zone assignment (upsert by zone) |
GET |
/api/v1/media-channels/:channelId/spaces |
List assigned spaces with device counts |
POST |
/api/v1/media-channels/:channelId/spaces |
Assign channel to a space |
DELETE |
/api/v1/media-channels/:channelId/spaces/:spaceId |
Unassign channel from space |
GET |
/api/v1/media-channels/:channelId/time-blocks |
List time blocks (filtered by date range) |
POST |
/api/v1/media-channels/:channelId/time-blocks |
Create time block |
PUT |
/api/v1/media-channels/:channelId/time-blocks/:blockId |
Update time block |
DELETE |
/api/v1/media-channels/:channelId/time-blocks/:blockId |
Delete time block |
GET |
/api/v1/media-channels/:channelId/fallback-config |
Get fallback config |
PUT |
/api/v1/media-channels/:channelId/fallback-config |
Upsert fallback config |
Layout Templates
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/layout-templates |
List templates (filter by orientation, delivery_profile) |
Ticketing Endpoints (/api)
Ticket Groups
Public endpoints (no auth required unless stated).
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/ticketGroups |
— | List active ticket groups (customer) |
GET |
/api/ticketGroups/ticketProfile |
— | Full ticket group profile |
GET |
/api/ticketGroups/ticketVariants |
— | Available variants |
GET |
/api/ticketGroups/ticketAvailability |
— | Availability per variant |
GET |
/api/ticketGroups/ticketServiceTypes |
— | Service type options |
GET |
/api/ticketGroups/attachment/:uniqueExtension |
— | Get cover image |
POST |
/api/ticketGroups |
ADMIN/SYSADMIN/MEMBER | Create ticket group |
PUT |
/api/ticketGroups/basicInfo |
ADMIN/SYSADMIN/MEMBER | Update basic info |
PUT |
/api/ticketGroups/image |
ADMIN/SYSADMIN/MEMBER | Replace cover image |
PUT |
/api/ticketGroups/placements |
ADMIN/SYSADMIN/MEMBER | Update display placements |
PUT |
/api/ticketGroups/details |
ADMIN/SYSADMIN/MEMBER | Update ticket details sections |
PUT |
/api/ticketGroups/variants |
ADMIN/SYSADMIN/MEMBER | Update pricing variants |
PUT |
/api/ticketGroups/organiserInfo |
ADMIN/SYSADMIN/MEMBER | Update organiser info |
POST |
/api/ticketGroups/gallery |
ADMIN/SYSADMIN/MEMBER | Upload gallery image |
DELETE |
/api/ticketGroups/gallery |
ADMIN/SYSADMIN/MEMBER | Delete gallery image |
Tags
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/tags |
— | List all tags |
POST |
/api/tags |
ADMIN/SYSADMIN/MEMBER | Create tag |
PUT |
/api/tags |
ADMIN/SYSADMIN/MEMBER | Update tag |
DELETE |
/api/tags |
ADMIN/SYSADMIN/MEMBER | Delete tag |
Banners
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/banners |
— | List active banners (customer) |
GET |
/api/banners/all |
ADMIN/MEMBER/SYSADMIN | List all banners (admin) |
GET |
/api/banners/attachment/:uniqueExtension |
— | Get banner image |
POST |
/api/banners |
ADMIN/MEMBER/SYSADMIN | Create banner (with image upload) |
PUT |
/api/banners |
ADMIN/MEMBER/SYSADMIN | Update banner |
DELETE |
/api/banners |
ADMIN/MEMBER/SYSADMIN | Delete banner |
PUT |
/api/banners/placements |
ADMIN/MEMBER/SYSADMIN | Reorder banners |
Rail Menus
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/railMenus/customer |
— | Customer view with ticket groups + ads |
GET |
/api/railMenus |
ADMIN/SYSADMIN/MEMBER | Admin view |
POST |
/api/railMenus |
ADMIN/SYSADMIN/MEMBER | Create rail menu |
PUT |
/api/railMenus |
ADMIN/SYSADMIN/MEMBER | Update rail menu |
DELETE |
/api/railMenus |
ADMIN/SYSADMIN/MEMBER | Delete rail menu |
PUT |
/api/railMenus/placements |
ADMIN/SYSADMIN/MEMBER | Reorder all rail menus |
POST |
/api/railMenus/ticketGroups |
ADMIN/SYSADMIN/MEMBER | Assign ticket groups to rail menu |
DELETE |
/api/railMenus/ticketGroups |
ADMIN/SYSADMIN/MEMBER | Remove ticket groups from rail menu |
Group Gallery
| Method | Path | Description |
|---|---|---|
GET |
/api/groupGallery/attachment/:uniqueExtension |
Get gallery image |
Time Block Scheduling
Time blocks define what playlist plays on a channel during a given window.
sequenceDiagram
participant Admin
participant CoreAPI
participant DB
Admin->>CoreAPI: POST /media-channels/:id/time-blocks
Note over Admin,CoreAPI: { name, blockType, startDate, endDate,\n startTime, endTime, recurrence,\n recurrenceDays, priority, playlistId }
CoreAPI->>DB: INSERT time_blocks
CoreAPI-->>Admin: 201 { id }
A default block (priority 0) is synthesised from the channel's zone assignment if no scheduled block covers a window.
Approval Flow (Media Channels)
stateDiagram-v2
[*] --> draft
draft --> pending_review: submit
pending_review --> approved: admin approves
pending_review --> rejected: admin rejects
rejected --> draft: edit & resubmit
approved --> active: go live
approvalStatus transitions via PATCH on the channel's approvalStatus field in PUT /media-channels/:id.
Key Creation Flows
Create a Playlist and Add Media
POST /api/v1/playlists
Header: X-Organization-ID: <org-uuid>
Body: { "name": "Morning Show" }
→ Returns: { "playlistId": "uuid" }
POST /api/v1/playlists/:playlistId/media
Body: [
{ "mediaId": "...", "position": 0, "duration": 30, "name": "...", "mediaType": "video" },
{ "mediaId": "...", "position": 1, "duration": 15, ... }
]
→ Clears existing items, inserts new ones, recalculates totalDuration + version
Media sync is replace-all
POST /playlists/:id/media deletes all existing items before inserting. Always send the full ordered list.
Create a Media Channel with a Base Playlist
POST /api/v1/media-channels
Headers: X-Organization-ID, X-User-ID
Body: {
"name": "Lobby Channel",
"orientation": "landscape",
"deliveryMode": "pre-cached",
"basePlaylistId": "playlist-uuid", ← optional: auto-creates zone assignment
"layoutTemplateId": "template-uuid" ← optional
}
→ If basePlaylistId provided: auto-creates channel_zone_assignment
(uses first zone of layoutTemplate, or falls back to channelId as zoneId)
→ Returns: { "id": "channel-uuid" }
Assign Channel to a Space (Push to Devices)
POST /api/v1/media-channels/:channelId/spaces
Headers: X-User-ID
Body: { "spaceId": "location-uuid" }
→ Inserts channel_space_assignments (ON CONFLICT DO NOTHING)
→ Queries playlist items from channel's zone assignment
→ For each device in the space: publishes MQTT to
topic: "tellyboard/devices/{deviceId}/play"
payload: { id, playlistName, items: [...] }
→ Returns: { message, devices, sentCount, playlistId }
Schedule a Time Block
POST /api/v1/media-channels/:channelId/time-blocks
Headers: X-User-ID
Body: {
"name": "Lunch Promo",
"blockType": "scheduled",
"playlistId": "playlist-uuid",
"startDate": "2026-06-01",
"endDate": "2026-08-31",
"startTime": "11:30",
"endTime": "13:30",
"recurrence": "weekly",
"recurrenceDays": ["mon","tue","wed","thu","fri"],
"priority": 20,
"color": "#f59e0b"
}
→ Returns: { "id": "block-uuid" }
Priority
Higher priority wins when blocks overlap. Default blocks (from zone assignments) always have priority: 0.