Skip to content

bensblueprints/signboard

Repository files navigation

📺 Signboard

License: MIT

Every screen you own, one price, forever.

Self-hosted digital signage — build playlists of images, videos, web pages, and templated announcement slides; pair any screen (any browser, any device) with a 6-character code; schedule content by time of day; watch every screen's health from one dashboard. No per-screen SaaS rent, no app to install on the TV.

Screenshot

☕ Skip the setup — get the 1-click installer

Don't want to touch a terminal? Grab the packaged installer (Windows desktop app + guided VPS deploy) here:

https://whop.com/onetime-suite — one-time purchase, lifetime updates.

Features

  • Content library — image upload, video upload (mp4/webm), web-page URL (sandboxed iframe, with a dashboard flag if a site blocks embedding), and announcement slides built from 5 layout templates (basic-center, banner-top, split-left, quote, alert) so a "Closed Friday" slide takes 20 seconds, no designer required
  • Playlists — ordered items with per-entry duration overrides, drag-to-reorder, cut/fade transition, in-browser preview
  • Screen pairing — the player page (/player) shows a 6-character pairing code; type it into the dashboard, name the screen, assign a playlist. No per-screen accounts, no player app — any smart-TV browser, a Pi in kiosk Chromium, or an old tablet all work identically
  • Server-side dayparting — schedule rules ("Mon–Fri 07:00–11:00 → Breakfast Menu") evaluated in the screen's own IANA timezone, not the kiosk device's (often wrong) clock. Priority-ordered rules, visual weekly grid in the admin UI
  • Screen health dashboard — live grid: online (WS connected or heartbeat < 90s) / offline, last-seen timestamp, currently-playing item, reported resolution, player version
  • Live updates — playlist/schedule/takeover edits push to affected players instantly over WebSocket (reload_manifest); players also poll every 60s as a fallback
  • Offline caching — the player registers a service worker that pre-caches the current playlist's media via Cache Storage, so a dropped connection mid-loop doesn't blank the screen; cache is evicted of anything no longer in the manifest so a video-heavy playlist doesn't fill a Pi's SD card
  • Emergency takeover — push one slide to every paired screen instantly; clear with one click
  • 100% local & private — one SQLite file + an uploads folder, no telemetry, no external services

⚠️ Service workers require HTTPS or localhost

Offline media caching uses the browser Service Worker API, which browsers only allow on https:// origins or localhost/127.0.0.1. If you're deploying on a plain-http:// LAN address (e.g. http://192.168.1.50:5338/player on a bare Pi), the player degrades gracefully to no-cache mode — it still plays fine, it just re-fetches media from the network every loop instead of caching it. For real offline resilience on a LAN, put a reverse proxy (Caddy/nginx/Traefik) with a local CA or Tailscale/self-signed cert in front of the signage box, or just run the player through npm run desktop (Electron), which loads 127.0.0.1 and gets full caching for free.

Quick start

npm i
npm run build   # builds the admin UI
npm start       # → http://localhost:5338
  • Admin panel: http://localhost:5338/admin (default password admin — change via ADMIN_PASSWORD)
  • Player page: http://localhost:5338/player — open this on the TV/tablet/Pi you want to pair

Desktop mode

Run it as a desktop app, or deploy to a $5 VPS when you need it public:

npm run desktop   # Electron window, auto-logged-in, data stored per-user

npm run desktop also doubles as a literal kiosk player — point a signage Windows box at it and fullscreen /player inside the window.

npm run dist packages a Windows installer (NSIS) via electron-builder.

Docker (VPS / always-on deploy)

cp .env.example .env   # set ADMIN_PASSWORD!
docker compose up -d   # persists SQLite + uploads in a named volume

Put Caddy/nginx/Traefik in front for TLS — required if you want players to get real offline media caching (see the HTTPS caveat above).

Signboard vs the SaaS competition

Signboard (this) Yodeck ScreenCloud OptiSigns Xibo Cloud
Price $49 once $8/screen/mo $20+/screen/mo $10/screen/mo ~$12/display/mo
5-screen annual cost $0 $480/yr $1,200/yr $600/yr $720/yr
Own your data Always On their servers On their servers On their servers On their servers
Pairing 6-char code, any browser Dedicated player app Dedicated player app Dedicated player app Player app / Xibo client
Dayparting ✅ Server-side, per-screen timezone
Offline caching ✅ Service worker
Emergency takeover ✅ Built in Paid tier Paid tier Limited
Source MIT, self-hosted Closed Closed Closed Open core
Cost over 3 years (5 screens) $49 $1,440 $3,600 $1,800 $2,160

Tech stack

  • Server: Node 20+, Express, better-sqlite3 (WAL), ws WebSocket server on the same HTTP server — single process serves admin, API, player, and media
  • Admin UI: React 18, Vite, Tailwind CSS 4, Framer Motion (drag-to-reorder, tab transitions), Lucide icons
  • Player: framework-light plain JS (ES2017-safe for ancient smart-TV browsers), double-buffered DOM swap to avoid flicker, muted-autoplay video, service worker for offline media
  • Desktop: thin Electron wrapper reusing the exact same server on a free local port
  • Storage: one SQLite file + an uploads folder. Back up = copy two things.

Configuration

Env var Default Purpose
PORT 5338 Server port
ADMIN_PASSWORD admin Admin panel password
DB_PATH ./data SQLite db + uploads live inside this directory
UPLOAD_DIR <DB_PATH>/uploads Override just the uploads location
BASE_URL request host Absolute base URL used to build media links (needed behind some reverse proxies)
FAKE_NOW ISO datetime override for the schedule evaluator — testing only

Data model

items (image/video/url/slide) · playlists + playlist_items (ordered, per-item duration override) · screens (device token, pairing code, IANA timezone, default playlist, last-seen/state) · schedule_rules (per-screen daypart rules with days-of-week bitmask, start/end time, priority) · takeover (single-row emergency override) · sessions (admin auth).

Development

npm start        # API + player on :5338
npm run dev      # Vite dev server for the admin UI on :5339 (proxies /api, /media, /player)
npm test         # end-to-end smoke test — spawns the real server on :5438 against a temp DB

License

MIT © 2026 Ben (bensblueprints)

About

Self-hosted digital signage — pairing-code screens, playlists with dayparting per screen timezone, health dashboard, offline-caching browser player. Yodeck alternative: pay once, no per-screen fee.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages