26 — Signage & Digital Displays (product: Beacon)
beacon.gopastearth.com. Admin config lives under the dashboard host.
Code and API paths may still say signage during transition (/api/signage/*, SignagePlayer.jsx).
Domain architecture and cutover phases: Chapter 31.
CEO Summary
Beacon (legacy docs: Signage) lets each tenant manage in-venue digital ads: folders, individual creatives (image, video, URL iframe, HTML snippet), and displays (named players with stable URLs). Data lives in the same per-tenant MySQL as the rest of the app — no separate signage database. Business value: Menu boards, promo loops, lobby screens without a third-party digital-signage contract. Bridge: When the on-prem bridge is connected, catalog changes push a WebSocket signageUpdate so local browsers or custom players can refresh immediately. If this breaks: Admin can still edit the catalog; TVs using the public player only stop updating until the API is back (optional ?reloadSec= on the player URL polls the cloud).
Signage in the platform stack
Fullscreen players are first-class HTTPS clients: they load the public player route and poll or fetch the playlist from the same Nginx → Express path as the marketing site and dashboard SPA. They are called out on the 30 — Full-stack architecture deploy diagram. They do not require the on-site bridge for basic playback; the bridge adds LAN-side instant refresh (WebSocket signageUpdate) when connected — see 13 — Bridge & Devices.
Architecture
/:tenantSlug/signage/:displaySlug"] end subgraph API["Express"] SR["signage.routes.js
/api/signage"] BR["bridge.routes.js
POST /bridge/notify-signage"] BCM["bridgeConnectionManager
pushEnvelope"] end subgraph Data["Tenant MySQL"] F["SignageFolders"] A["SignageAds"] D["SignageDisplays"] end subgraph Bridge["On-prem Bridge"] WS["WebSocket client"] end UI -->|REST| SR PL -->|GET playlist JSON| SR SR --> F SR --> A SR --> D SR -->|catalogChanged| BCM BR --> BCM BCM -->|signageUpdate JSON| WS
REST API
Mount: app.use('/api/signage', signageRouter) in server.js.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/signage/playlist/:tenantSlug/:displaySlug | None | JSON playlist for the player (resolves tenant via master Tenants, then tenant DB) |
| GET/POST | /api/signage/folders | Session + tenant | List / create folders |
| PUT/DELETE | /api/signage/folders/:id | Session + tenant | Update / delete (ads in folder become unfiled on delete) |
| GET/POST | /api/signage/ads | Session + tenant | List (optional ?folderId= / null) / create ad |
| PUT/DELETE | /api/signage/ads/:id | Session + tenant | Update / delete |
| GET/POST | /api/signage/displays | Session + tenant | List / create display (slug, optional root_folder_id) |
| PUT/DELETE | /api/signage/displays/:id | Session + tenant | Update / delete |
Tenant database tables
Created idempotently on first use (CREATE TABLE IF NOT EXISTS) in ensureSignageTables():
SignageFolders—org_id,parent_id,name,sort_orderSignageAds—media_type(image | video | url | html),media_url,html_body,duration_seconds,orientation,is_active,metadata(JSON)SignageDisplays—slug(unique per org),root_folder_id(NULL = all active ads),orientation(landscape | portrait | inherit)
Frontend
- Admin:
frontend/src/pages/admin/SignageAdsCatalog.jsx—/admin/signage(nav: Catalog → Signage & ads). Bridge status, notify bridge, CRUD. - Player:
frontend/src/pages/signage/SignagePlayer.jsx—/:tenantSlug/signage/:displaySlug. Rotates slides; optional?reloadSec=/?reloadsec=/?poll=(15–3600) to re-fetch playlist from cloud; optional?bridgeWs=or?bridgeWS=(URL-encodedws/wssURL to bridge/ws) for push refresh when mixed content allows.
Bridge integration
See 13 — Bridge & Devices for WebSocket setup. Signage-specific behavior:
bridgeConnectionManager.pushEnvelope— fire-and-forget JSON to the tenant’s connected bridge.- After every successful folder/ad/display mutation, server sends
{ type: "signageUpdate", action: "catalogChanged", hints: { playlistApiPath, webPlayerPath, cloudOrigin?, … } }. POST /api/bridge/notify-signage— manual refresh; optional body{ displaySlug, reason }; actionmanualRefresh.- Hints: Built by
backend/utils/publicOrigin.js(signageBridgeHints) usingX-Forwarded-Proto/ Host when behind a proxy. - Bridge-hosted LAN player: Node bridge serves
GET /signage-relay?tenantSlug=…&displaySlug=…(static page underbridge/public/) plusGET /local/signage/playlist/:tenant/:displayproxying the public cloud playlist — HTTP on the LAN so TVs can usews://to the same bridge without browser mixed-content blocking. Optional:kiosk=1(hide status),reloadSec/reloadsec/pollfor polling.
Sequence: catalog save → bridge → local player
Source files
backend/routes/signage.routes.jsbackend/routes/bridge.routes.js—notify-signagebackend/utils/publicOrigin.js—signageBridgeHintsbackend/services/bridgeConnectionManager.js—pushEnvelope
Roadmap (not in current slice)
Daypart schedules, calendar campaigns, device registry heartbeats, and fine-grained RBAC for signage admin are natural extensions; the current module is catalog + player + bridge hooks.