Skip to content
09Case studySW · 09 of 10

Three runtimes, one team, one month: a digital signage network for venue, advertiser, and screen.

Three audiences, one product. Venue operators couldn't manage screen schedules, advertisers couldn't buy inventory, and the screens themselves needed a player that fit heterogeneous hardware. We built all three surfaces in 27 days.

ClientConfidential
Year2026
Duration1 yrs
StackTypeScript · Next.js · React · PHP · Laravel · C++ · Qt · Supabase · AWS
Hero image for ooh-signage-networkFIG 01 · HERO

Three audiences, one product, zero shared assumptions.

An OOH advertising network has three distinct users with nothing in common. Venue owners want to know what's running on their screens and when. Advertisers want to buy slots, target by location, and see impression counts. The screens themselves aren't users at all — they're devices that need to receive schedules reliably and play them without human intervention.

The client had a working concept and a tight launch window. They needed a marketplace backend to handle advertiser approvals, billing, and campaign scheduling. A console for operators and venue owners to drag slots into a calendar. And a player runtime that could run on Android tablets behind a hotel front desk, Linux kiosks in a transit hub, and Windows set-top boxes in a retail chain — without deploying a different codebase to each.

The convenient default for the player was Electron. Electron packages a Chromium instance and a Node runtime, and it runs everywhere. It also weighs hundreds of megabytes, auto-updates unpredictably, and adds a browser's worth of complexity to what should be a media player. On constrained kiosk hardware, the footprint mattered. The update story mattered more.

A marketplace backend, a drag-and-drop console, and a native player that runs on four operating systems.

The backend is a multi-tenant Laravel 12 application handling everything the marketplace needs: advertiser onboarding and approvals, campaign scheduling with day-parting and geo-targeting, Stripe billing reconciliation, media upload and transcode queued through Laravel's background job layer, and an impression aggregation model that feeds reporting. Sanctum handles API authentication. Assets live on S3.

The operator console is a Next.js 16 admin surface with a drag-and-drop scheduler built on @dnd-kit. Venue owners see their screens and their slots. Advertisers see their campaigns and their reach. The admin role sees everything. All three are the same application, gated by role.

The player is Qt 6 and C++20. It runs on macOS, Linux, Windows, and Android from a single CMake codebase with per-platform presets. On startup it connects to Supabase via WebSockets and subscribes to its schedule channel. When the operator moves a slot in the console, the change arrives at the screen in real time — no polling, no refresh cycle. If the network drops, the player falls back to its locally cached schedule and media. The screen keeps playing.

F · 01Multi-tenant console
Venue owners, advertisers, and admin in one surface, gated by role. No separate portals, no duplicated codebases.
F · 02Drag-and-drop scheduler
Slots, geo-fences, and day-parts arranged in a calendar built on @dnd-kit. Operators see conflicts before they save.
F · 03Real-time player sync
Schedule changes push to the player fleet via Supabase WebSockets the moment the operator saves. No polling, no restart.
F · 04Offline-capable player
Qt Sql with SQLite caches the current schedule and media locally. A Wi-Fi drop doesn't blank the screen.
F · 05Cross-platform native binary
One CMake codebase, four target platforms: macOS, Linux, Windows, Android. Each build is a self-contained native binary — no Chromium, no V8.
F · 06Marketplace billing
Stripe billing reconciliation and impression aggregation in the Laravel backend. Advertisers pay for confirmed impressions.

The Qt choice, and what it cost against the Electron default.

Electron was the obvious call. The whole team knew JavaScript. The admin console was already in TypeScript. An Electron player would have shared a third of its codebase with the web surface. We ruled it out in the first week.

The deployment reality was the deciding factor. The target hardware ranged from Android tablets on 4G to Windows mini-PCs on enterprise Wi-Fi to Linux kiosks with locked-down OS images. Electron's Chromium footprint — typically 150–300 MB installed — was a hard no for the constrained devices. Chromium's auto-update behaviour, which fights with locked-down kiosk OS policies, was a harder no for the enterprise deployments.

Qt 6 with C++20 gives a native binary per platform, compiled to roughly 15 MB on Linux and under 30 MB on Windows and Android. The update path is a file swap, orchestrated by the player itself. There is no browser engine, no V8, no Electron protocol broker. The WebSocket connection to Supabase runs through Qt Network; media playback runs through Qt Multimedia. The offline cache uses Qt Sql with SQLite. Every dependency ships in the binary.

A player that fits the hardware beats a player that fits the team.

The cost was context-switching. The team ran three runtimes — PHP, TypeScript, C++ — in parallel. Cross-runtime integration tests required more scaffolding than a single-language stack would. The CMake build matrix for four target platforms added CI complexity. GitHub Actions runs four player build jobs per push; the Docker-based Linux build proved the most reliable baseline for reproducibility.

A three-tier OOH platform, shipped in under a month.

The first version of all three surfaces was production-ready in 27 days: 223 commits across three repos, multi-platform CI on every push, player binaries confirmed on all four target operating systems before the final review. The marketplace backend handles advertiser billing reconciliation and impression aggregation. The console handles scheduling without a spreadsheet. The player handles network drops without blanking the screen.

The platform is live. The operator console is the system of record for screen schedules across the network. The player fleet receives updates in real time via Supabase WebSockets — schedule changes made in the console propagate to physical screens without a restart or a manual sync.

The moral of the stack choice is simple. Electron is the right answer when the team's comfort and delivery speed are the binding constraints. It was not the right answer here. Pick the player runtime that matches your hardware, not the one that matches your team's comfort zone.

PULL QUOTE / 04
We told them the player had to run on everything we'd already bought. They came back with a binary that did.
FounderOut-of-home advertising network
Outcome
Days to first production build
27
Repos shipped in parallel
3
Player target platforms
4
Real-time push (WebSocket)
< 1 s
NEXTCase study 10SW · 10 of 10
Consumer wellness2018 — 2019

When the platform doesn't speak your business, you write the modules that do.