Communitygithub.com

timerise-ai/digital-signage

Agent Skill: build a digital-signage module — media library, per-screen playlists, display registry, PIN and provisioning-URL device pairing, fullscreen TV player with stall recovery, screen health and remote control — in Next.js App Router on Firestore or Supabase

What is digital-signage?

digital-signage is a Claude Code agent skill that agent Skill: build a digital-signage module — media library, per-screen playlists, display registry, PIN and provisioning-URL device pairing, fullscreen TV player with stall recovery, screen health and remote control — in Next.js App Router on Firestore or Supabase.

Works with~Claude Code~Codex CLI~Cursor
npx skills add timerise-ai/digital-signage

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Digital Signage — Displays + Ads

A screen in a venue is not a web page. It runs unattended for months on a cheap TV stick, nobody is watching the console, and the failure mode is a frozen frame that no one notices for a week. Every decision below exists to survive that.

This skill carries a complete module: admin CRUD for media and playlists, a display registry, PIN and provisioning-URL pairing, a fullscreen player, and the operational layer (health, preview, remote control) that makes it manageable.

When to use

Building or extending screens that loop media in a physical space, on Next.js App Router with Firestore or Supabase/Postgres.

When NOT to use

  • Web ad serving (impressions, bidding, tracking pixels, third-party tags) — a different problem with different infrastructure.
  • Interactive kiosks where the user taps to transact. Signage is one-way.
  • A single embedded video on a marketing page. Use a <video> tag.
  • Generic Next.js, CMS, or auth setup — assumed to exist.

Architecture

 ADMIN (browser)                     SERVER                      DEVICE (TV)
 ─────────────────                   ──────                      ───────────
 media upload ──────────────► object storage ◄──── public/signed URL ─┐
 ad CRUD ─────────┐                                                   │
 playlist edit ───┼──► /api/admin/*  ──► ads + displays ──┐           │
 issue command ───┘      (staff auth)      (scoped)       │           │
                                                          ▼           │
 pair screen ◄──── PIN ────────────► /api/display/pair    │           │
                                     mints per-display    │           │
                                     token                ▼           │
                                    /api/display/playlist ─── poll ───┤
                                     ▲ resolves adIds → ads           │
                                     └── carries telemetry up,        │
                                         commands down          player loop

One request type sustains the whole runtime: the device polls /api/display/playlist, sending telemetry up and receiving content and commands down. Everything operational rides that channel.

Critical facts — read before designing anything

  1. The playlist is an ordered array on the display, not a separate entity. display.adIds: string[] is the playlist — order is free, no joins, one read. Introduce a standalone playlist entity only when the same content must run on several screens; see data-model.md for the trade-off and extensions.md for the migration.
  2. Poll; do not stream. A 30–60 s poll is cheaper, survives sleeping network stacks, and reconnects for free. Realtime is an optional upgrade, not the baseline. Cache the response with an ETag or you will pay for a full read per screen per poll.
  3. Media uploads go browser → storage directly, never through an API route. Route handlers have body-size limits and burn compute proxying bytes.
  4. The device is untrusted and unattended. It holds a long-lived credential in localStorage on hardware anyone can walk up to. That credential must be per-screen and revocable.
  5. The player must never be able to stop. Every media element gets a timeout, an error handler, and a way to skip. A broken asset advances; it does not wedge.

Hard rules

Never derive the poll interval from render state. Poll on a stable interval and read slide state from a ref. A timer whose effect depends on the slide index is destroyed and recreated on every slide, so a refresh longer than a slide would never fire; the behaviour contract pins the poll's timing to wall-clock time.

Never overload one boolean as both "paused" and "deleted". Use active for operator intent and a separate deletedAt for lifecycle, or a deleted item reappears the moment someone toggles it back on.

Never trust a client-supplied tenant/location scope. Derive it server-side from the authenticated staff session or from the display row the token resolves to, and enforce it on every [id] route — returning 404, not 403, so ids cannot be probed.

Never issue one shared token to every screen. Mint a per-display token at pairing, store only its hash, and make revocation a single row update.

Never start an image's duration timer before onLoad. On a slow TV the slide will otherwise expire before it is visible.

Never carry durable screen state in a one-shot command. Blank and takeover must survive the device's daily reload and a power cycle — deliver them as state on every poll (mode), and keep the ack-cleared command channel for genuine one-shots like reload. And any command whose effect is a reload must persist its ack before reloading, or the server re-delivers it forever.

Quick start

  1. Fill in the seam contract for your app and confirm the domain rename — adaptation.md.
  2. Model the entities and pick array-vs-junction — data-model.md.
  3. Create tables/collections, indexes, security rules and the media bucket — firestore-backend.md or supabase-backend.md.
  4. Build the device and admin endpoints, including pairing and token verification — api-routes.md.
  5. Get a screen paired — pairing.md — then drop in the player loop — player-runtime.md.
  6. Build the back-office: media library, display list, playlist editor — admin-ui.md.
  7. Add health, preview and remote control before going live — operations.md.
  8. Verify against the behaviour contract table in player-runtime.md — pull the network cable, delete the current ad mid-loop, issue a reload — and ship the player-machine tests as regression cover.

Reference directory

Load the reference matching the trigger keywords. For greenfield design, read data-model.md first.

Code in api-routes.md and operations.md is written against Firestore as the canonical backend; supabase-backend.md defines every substitution — the junction table replacing adIds, and SQL equivalents of the service functions.

ScenarioTrigger keywordsReference
Fitting this into an existing appadapt, rename, seam, integrate, tenant, host appadaptation.md
Entities, fields, playlist shapeschema, model, Ad, Display, playlist, junction, soft deletedata-model.md
Firestore/Firebase backendFirestore, firebase-admin, security rules, composite index, Firebase Storagefirestore-backend.md
Supabase/Postgres backendSupabase, Postgres, RLS, migration, storage bucket, Realtimesupabase-backend.md
Endpoints, pairing, tokens, cachingAPI route, pairing, PIN, token, ETag, scope check, zodapi-routes.md
Getting a screen pairedpairing, PIN, provisioning URL, hub, credential, unpairpairing.md
The TV player loopplayer, loop, crossfade, video stall, rotation, portrait, wake lock, offlineplayer-runtime.md
Back-office UIadmin, upload, media library, playlist editor, provisioning URLadmin-ui.md
Health, preview, remote controlheartbeat, last seen, offline, preview, reload, blank, emergency takeover, audit logoperations.md
Scheduling, reuse, reportingdayparting, start date, campaign, shared playlist, proof of play, precache, multi-zoneextensions.md
Why the templates differ from a naive portprovenance, ledger, rationale, kept, addedprovenance.md

Related Skills