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