Skip to content

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
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.