Universal Digital Signage

Universal Digital Signage

Containerized Architecture with Schema Definitions and Docker Compose Orchestration

This is a multi-container, containerized application managed by Docker Compose: which is referred to interchangeably as a Compose application or project (sometimes 'stack,' though that’s more Swarm terminology). In more architectural terms: a three-tier containerized application stack consisting of: MongoDB (data layer), Node.js backend (API/business-logic layer), React frontend (presentation layer). All three run in their own containers and are wired together by a single docker-compose.yml.


Docker Basics

Docker Compose puts all services on a private network so they can talk to each other by service name, without exposing internal ports to the outside world. Internal networking When you do docker-compose up, Compose creates a network (universal-signage_default) and attaches mongodb, the backend and the frontend to it. From inside the backend container, MongoDB is reached at host mongodb on port 27017. From outside the app container, such as when you use Mongosh to connect to and manage the database - you would use port 27018 as it is exposed. From inside the frontend container, the frontend reaches the backend at host backend on port 3001 (the API) and serves the appplication itself to the user on port 3002.

mongodb:     "27018:27017"   # so you can mongo-shell into it from your laptop as localhost:27018  
app:         "3001:3001"     # so you can hit your Node API at <your-server>:3001  
frontend:    "3002:3002"     # so you can hit your React dev server at <your-server>:3002

These mappings only affect traffic coming into Docker fthrough the user interaction: they don’t change how the containers talk to one another.

An Overview of the Database Schema

Collection and Key Fields

Collection Primary Fields Notes / FKs
departments _id (ObjectId), name (string)
users _id, firstName, lastName, email, password, role departmentId → departments._id
groups _id, name, departmentId departmentId → departments._id
devices _id, name, ip, location, groupId groupId → groups._id
channels _id, name, description
medias _id, url, type, tags
templates _id, name, htmlTemplate
styles _id, name, cssRules
playlists _id, name, templateId, styleId templateId → templates._id; styleId → styles._id
slides _id, playlistId, mediaId, order, contentType playlistId → playlists._id; mediaId → medias._id
assignplaylists _id, playlistId, deviceId m–n mapping between playlists & devices
schedulers _id, deviceId, playlistId, startTime, endTime, rrule deviceId → devices._id; playlistId → playlists._id
logrequests _id, deviceId, timestamp, route, statusCode deviceId → devices._id

JSON Schema Validators

Validators for all collections:

// 1. departments
db.createCollection('departments', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Department name is required'
        },
      }
    }
  }
});

// 2. users
db.createCollection('users', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['firstName', 'lastName', 'email', 'password', 'role', 'departmentName'],
      properties: {
        firstName: {
          bsonType: 'string',
          description: 'First name is required'
        },
        lastName: {
          bsonType: 'string',
          description: 'Last name is required'
        },
        email: {
          bsonType: 'string',
          pattern: '^.+@.+$',
          description: 'Email is required and must be valid'
        },
        password: {
          bsonType: 'string',
          description: 'Password is required'
        },
        bio: {
          bsonType: 'string',
          description: 'Bio is optional'
        },
        role: {
          bsonType: 'string',
          enum: ['standard', 'admin', 'assetManager', 'globalAssetManager'],
          description: 'Role must be one of the defined values'
        },
        departmentName: {
          bsonType: 'string',
          description: 'Department name is required'
        },
        departmentId: {
          bsonType: 'objectId',
          description: 'Reference to Departments collection'
        },
        profileImg: {
          bsonType: 'string',
          description: 'Profile image URL is optional'
        }
      }
    }
  }
});

// 3. groups
db.createCollection('groups', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'departmentId'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Group name is required'
        },
        departmentId: {
          bsonType: 'objectId',
          description: 'Must reference a department _id'
        },
      }
    }
  }
});

// 4. devices
db.createCollection('devices', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'ip', 'location', 'groupId'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Device name is required'
        },
        ip: {
          bsonType: 'string',
          description: 'Device IP address is required'
        },
        location: {
          bsonType: 'string',
          description: 'Human‐readable location is required'
        },
        groupId: {
          bsonType: 'objectId',
          description: 'Must reference a group _id'
        },
      }
    }
  }
});

// 5. channels
db.createCollection('channels', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'description'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Channel name is required'
        },
        description: {
          bsonType: 'string',
          description: 'Channel description is required'
        },
      }
    }
  }
});

// 6. medias
db.createCollection('medias', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['url', 'type'],
      properties: {
        url: {
          bsonType: 'string',
          pattern: '^https?://',
          description: 'Media URL must be a valid HTTP(S) link'
        },
        type: {
          bsonType: 'string',
          enum: ['image', 'video', 'html'],
          description: 'Type must be one of: image, video, html'
        },
        tags: {
          bsonType: 'array',
          items: { bsonType: 'string' },
          description: 'Optional array of tag strings'
        }
      }
    }
  }
});

// 7. templates
db.createCollection('templates', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'htmlTemplate'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Template name is required'
        },
        htmlTemplate: {
          bsonType: 'string',
          description: 'Raw HTML template is required'
        }
      }
    }
  }
});

// 8. styles
db.createCollection('styles', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'cssRules'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Style name is required'
        },
        cssRules: {
          bsonType: 'string',
          description: 'CSS text for this style'
        }
      }
    }
  }
});

// 9. playlists
db.createCollection('playlists', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['name', 'templateId', 'styleId'],
      properties: {
        name: {
          bsonType: 'string',
          description: 'Playlist name is required'
        },
        templateId: {
          bsonType: 'objectId',
          description: 'Must reference a template _id'
        },
        styleId: {
          bsonType: 'objectId',
          description: 'Must reference a style _id'
        }
      }
    }
  }
});

// 10. slides
db.createCollection('slides', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['playlistId', 'mediaId', 'order', 'contentType'],
      properties: {
        playlistId: {
          bsonType: 'objectId',
          description: 'Must reference a playlist _id'
        },
        mediaId: {
          bsonType: 'objectId',
          description: 'Must reference a media _id'
        },
        order: {
          bsonType: 'int',
          minimum: 0,
          description: 'Zero‐based position in playlist'
        },
        contentType: {
          bsonType: 'string',
          description: 'E.g. "image", "video", etc.'
        }
      }
    }
  }
});

// 11. assignplaylists
db.createCollection('assignplaylists', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['playlistId', 'deviceId'],
      properties: {
        playlistId: { bsonType: 'objectId' },
        deviceId:   { bsonType: 'objectId' }
      }
    }
  }
});

// 12. schedulers
db.createCollection('schedulers', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['deviceId', 'playlistId', 'startTime', 'endTime', 'rrule'],
      properties: {
        deviceId: {
          bsonType: 'objectId',
          description: 'Must reference a device _id'
        },
        playlistId: {
          bsonType: 'objectId',
          description: 'Must reference a playlist _id'
        },
        startTime: {
          bsonType: 'date',
          description: 'When playback starts'
        },
        endTime: {
          bsonType: 'date',
          description: 'When playback ends'
        },
        rrule: {
          bsonType: 'string',
          description: 'Recurrence rule in iCal RRULE format'
        }
      }
    }
  }
});

// 13. logrequests
db.createCollection('logrequests', {
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['deviceId', 'timestamp', 'route', 'statusCode'],
      properties: {
        deviceId: {
          bsonType: 'objectId',
          description: 'Must reference a device _id'
        },
        timestamp: {
          bsonType: 'date',
          description: 'When request occurred'
        },
        route: {
          bsonType: 'string',
          description: 'Requested API route'
        },
        statusCode: {
          bsonType: 'int',
          description: 'HTTP status code returned'
        }
      }
    }
  }
});

Mongoose Schemas

The blueprint used to define the shape, defaults, validation rules, and behavior of the documents in a MongoDB collection — in the Node.js application:

const mongoose = require('mongoose');

// 1. Department Schema
const departmentSchema = new mongoose.Schema({
  name: { type: String, required: true }
});

// 2. User Schema
const userSchema = new mongoose.Schema({
  firstName:   { type: String, required: true },
  lastName:    { type: String, required: true },
  email:       { type: String, required: true, match: /^.+@.+$/ },
  password:    { type: String, required: true },
  bio:         { type: String },
  role:        { type: String, required: true, enum: ['standard','admin','assetManager','globalAssetManager'] },
  departmentName: { type: String, required: true },
  departmentId:   { type: mongoose.Schema.Types.ObjectId, ref: 'Department' },
  profileImg:   { type: String }
});

// 3. Group Schema
const groupSchema = new mongoose.Schema({
  name:         { type: String, required: true },
  departmentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Department', required: true }
});

// 4. Device Schema
const deviceSchema = new mongoose.Schema({
  name:     { type: String, required: true },
  ip:       { type: String, required: true },
  location: { type: String, required: true },
  groupId:  { type: mongoose.Schema.Types.ObjectId, ref: 'Group', required: true }
});

// 5. Channel Schema
const channelSchema = new mongoose.Schema({
  name:        { type: String, required: true },
  description: { type: String, required: true }
});

// 6. Media Schema
const mediaSchema = new mongoose.Schema({
  url:  { type: String, required: true, match: /^https?:\/\// },
  type: { type: String, required: true, enum: ['image','video','html'] },
  tags: [{ type: String }]
});

// 7. Template Schema
const templateSchema = new mongoose.Schema({
  name:         { type: String, required: true },
  htmlTemplate: { type: String, required: true }
});

// 8. Style Schema
const styleSchema = new mongoose.Schema({
  name:     { type: String, required: true },
  cssRules: { type: String, required: true }
});

// 9. Playlist Schema
const playlistSchema = new mongoose.Schema({
  name:       { type: String, required: true },
  templateId: { type: mongoose.Schema.Types.ObjectId, ref: 'Template', required: true },
  styleId:    { type: mongoose.Schema.Types.ObjectId, ref: 'Style', required: true }
});

// 10. Slide Schema
const slideSchema = new mongoose.Schema({
  playlistId:  { type: mongoose.Schema.Types.ObjectId, ref: 'Playlist', required: true },
  mediaId:     { type: mongoose.Schema.Types.ObjectId, ref: 'Media', required: true },
  order:       { type: Number, required: true, min: 0 },
  contentType: { type: String, required: true }
});

// 11. AssignPlaylist Schema
const assignPlaylistSchema = new mongoose.Schema({
  playlistId: { type: mongoose.Schema.Types.ObjectId, ref: 'Playlist', required: true },
  deviceId:   { type: mongoose.Schema.Types.ObjectId, ref: 'Device',   required: true }
});

// 12. Scheduler Schema
const schedulerSchema = new mongoose.Schema({
  deviceId:   { type: mongoose.Schema.Types.ObjectId, ref: 'Device',   required: true },
  playlistId: { type: mongoose.Schema.Types.ObjectId, ref: 'Playlist', required: true },
  startTime:  { type: Date, required: true },
  endTime:    { type: Date, required: true },
  rrule:      { type: String, required: true }
});

// 13. LogRequest Schema
const logRequestSchema = new mongoose.Schema({
  deviceId:   { type: mongoose.Schema.Types.ObjectId, ref: 'Device', required: true },
  timestamp:  { type: Date, required: true, default: Date.now },
  route:      { type: String, required: true },
  statusCode: { type: Number, required: true }
});

module.exports = {
  Department:     mongoose.model('Department', departmentSchema),
  User:           mongoose.model('User', userSchema),
  Group:          mongoose.model('Group', groupSchema),
  Device:         mongoose.model('Device', deviceSchema),
  Channel:        mongoose.model('Channel', channelSchema),
  Media:          mongoose.model('Media', mediaSchema),
  Template:       mongoose.model('Template', templateSchema),
  Style:          mongoose.model('Style', styleSchema),
  Playlist:       mongoose.model('Playlist', playlistSchema),
  Slide:          mongoose.model('Slide', slideSchema),
  AssignPlaylist: mongoose.model('AssignPlaylist', assignPlaylistSchema),
  Scheduler:      mongoose.model('Scheduler', schedulerSchema),
  LogRequest:     mongoose.model('LogRequest', logRequestSchema)
};

Architecture Diagram

Mermaid Flowchart of the Containerized Architecture

Architecture