Skip to content
 
 

Repository files navigation

FlipOff.

Turn any TV into a retro split-flap display. The classic flip-board look, without the $3,500 hardware. And it's free.

FlipOff Screenshot

What is this?

FlipOff is a free, open-source web app that emulates a classic mechanical split-flap (flip-board) airport terminal display — the kind you'd see at train stations and airports. It runs full-screen in any browser, turning a TV or large monitor into a beautiful retro display.

No accounts. No subscriptions. No $199 fee. Build it once and go.

Features

  • Realistic split-flap animation with mechanical character-stepping (top/bottom flap halves)
  • Multiple sound modes: Authentic (default), Soft, Joke (rubber duck + fart), Mute — with stereo panning per tile
  • Auto-rotating screens with shuffle and random mode
  • Admin dashboard for board geometry, screens, plugins and message overrides — reachable from any device on the network
  • Plugin screens: clock, current weather, 3-day forecast, GitHub stats, Quote of the Day, crypto prices
  • Real-time WebSocket push — message and rotation changes appear on every connected display instantly
  • Connection indicator, and automatic reconnection: a display survives a server restart without anyone touching it
  • Multi-board support — run multiple independent displays from one server
  • Display modes: Color, Matrix, Grayscale accent palettes
  • Loading line across the top of the screen showing time until the next message
  • Tiles sized to the viewport, so any grid the server is configured with fits at any width
  • Navbar hides itself until you hover it, so the board is chrome-free at rest
  • Fullscreen TV mode
  • Keyboard controls for manual navigation
  • Emoji support in messages (animate through random charset characters before landing)
  • Responsive from mobile to 4K displays
  • Vanilla HTML/CSS/JS frontend — no UI framework, just Vite for bundling

Running It

The display is a client of the backend — it reads its grid, charset, colours, timing and its entire rotation from the server's /api/config. board/dist is not deployable on its own; there is no static/no-server mode.

git clone https://github.com/<your-username>/flipoff.git
cd flipoff

# With Docker (recommended):
docker compose up --build

# Or without Docker (needs Node 24+, for node:sqlite):
pnpm install
pnpm build
pnpm -C backend start

The backend serves board/dist and admin/dist, so both frontends have to be built before they will show anything. For development, pnpm dev starts all three processes: the admin on http://localhost:5173, the board on 5174, and the backend on 8080. Run them together — a Vite server alone has no backend to read its config from. The admin's dev server is the front door: it proxies /api and /ws to the backend and /b to the board's, so http://localhost:5173 behaves exactly like the deployed app.

URL What
http://localhost:8080 Admin dashboard (network-wide, password-protected)
http://localhost:8080/boards Straight to a dashboard page — every admin page has its own URL
http://localhost:8080/b Public index of every board, linking to each one
http://localhost:8080/b/<board-slug> A board. main is the one a fresh install creates

The admin password is auto-generated on first run and printed to the console. Set it explicitly with the ADMIN_PASSWORD environment variable.

Boards and screens persist in a Docker volume (flipoff-data), and are edited in the admin. The seed in backend/src/config/seed.ts only ever applies to a board being created — changing it does nothing to an install that already has one.

Screens and Plugins

A board's rotation is a list of screens, managed in the admin dashboard. Two kinds:

  • Manual — fixed lines you type in. A new board seeds these from the messages array in backend/src/config/seed.ts.
  • Plugin — lines regenerated on a refresh interval.
Plugin Shows Needs
Date & Time Time, weekday, date, UTC offset A time zone
Open-Meteo Current Weather City, country flag, temperature, condition A city + country
Open-Meteo 3 Day Forecast Weekday, min/max temperature, condition A city + country
GitHub Repo Stats Stars, forks, watchers A repo
GitHub Open Work Open issues and pull requests A repo
Quote of the Day A daily quote API_NINJAS_API_KEY
Random Quote A random quote API_NINJAS_API_KEY
Crypto Prices Current prices for chosen pairs API_NINJAS_API_KEY

A new install seeds a clock screen alongside the quotes. Weather is opt-in because it needs a location: screens render on the server, so there is no per-viewer geolocation or time zone — both are explicit settings.

Writing your own plugin is one file plus one line in the registry — see PLUGINS.md.

Keyboard Shortcuts

Key Action
Enter / Space Next message
Arrow Left Previous message
Arrow Right Next message
F Toggle fullscreen
M Cycle sound mode (Authentic / Soft / Joke / Mute)
R Toggle random message order
C Cycle display mode (Color / Matrix / Grayscale)
Escape Exit fullscreen

How It Works

Each tile on the board is an independent split-flap element with top/bottom halves that animate through an ordered character-stepping sequence — just like a real mechanical board. Only tiles whose content changes between messages animate. Emojis are supported: tiles flip through random charset characters before snapping to the final emoji.

Sound is generated per-tile using extracted tick slices from a recorded split-flap audio clip, with stereo panning based on tile position. A master audio chain applies lowpass filtering, EQ, and compression across four sound profiles. Authentic mode is the default.

The browser holds no configuration of its own. constants.js blocks on /api/config at startup and retries until the server answers, so a display started before the backend simply waits and then comes up. After that, the server pushes message_state and config_state frames over /ws; content changes swap the rotation in place, and only a layout change — grid, charset, palette, animation timing — reloads the page.

File Structure

A pnpm workspace with three packages: board (the display), admin (the dashboard) and backend (the server). The two frontends share no code — they are separate Vite builds, served by the backend at /b/{slug} and / respectively.

flipoff/
  pnpm-workspace.yaml     — Workspace definition
  Dockerfile              — Container image (builds both frontends, runs the backend)
  docker-compose.yml      — Docker Compose with persistent volume
  PLUGINS.md              — Plugin development guide
  board/
    index.html            — The display page
    boards.html           — The public board index at /b
    vite.config.ts        — Build config: base '/b/', dev proxy to the backend
    public/
      images/             — Screenshot and other static images
    src/
      main.js             — Entry point, audio init, tile fitting, fullscreen, remote sync
      Board.js            — Tile grid, display modes, transitions
      Tile.js             — Split-flap flip with character stepping and emoji support
      SoundEngine.js      — Sound profiles, tick extraction, stereo panning
      MessageRotator.js   — Shuffle, random mode, remote override
      KeyboardController.js — Keyboard shortcuts (F, M, R, C, arrows, etc.)
      RemoteMessageSync.js — WebSocket sync and reconnection
      boardsIndex.js      — The /b index: fetches /api/boards and lists them
      config.js           — Board slug, /api/config fetch with retry, boot presentation
      statusScreen.js     — Connecting / failure / reconnecting screens
      flapAudio.js        — Embedded base64 audio clip
      audio/              — Joke mode: rubber duck squeak, fart finisher
      css/
        reset.css         — CSS reset
        layout.css        — Page layout, nav icon buttons, tooltips, loading bar, fullscreen
        board.css         — Board container, accent bars, shortcuts overlay
        boardsIndex.css   — The /b index page
        tile.css          — Split-flap tile halves and flip animations
        responsive.css    — Media queries for the chrome (tile sizing is main.js's job)
  admin/                  — React + TypeScript dashboard
    index.html            — App shell
    vite.config.ts        — Build config: base '/', dev proxy to the backend and board
    src/
      main.tsx            — Entry point and routes (/, /boards, /send, /settings)
      App.tsx             — Session gate and the board list
      api/                — Typed client and the admin API response shapes
      state/              — Board list, per-board workspace, status banner, router context
      components/         — Layout, login, screen modal, plugin schema fields
      pages/              — Home, Boards, a board's workspace (config + screens), Send, Settings
      lib/                — Formatting and screen helpers
      css/
        reset.css         — CSS reset (its own copy; the packages share nothing)
        admin.css         — Admin dashboard styles
  backend/
    src/
      main.ts             — Entry point, http server, WebSocket upgrade
      app.ts              — Express app: middleware, routes, static serving
      types.ts            — Shared state and screen types
      config/             — Data paths, app constants, and seed.ts (a new board's values)
      board/              — Serialization, screens, registry, validation
      db/                 — SQLite: boards, screens, migrations, first-run seed
      auth/               — bcrypt password handling and admin sessions
      ws/                 — WebSocket hub and per-board broadcast
      routes/             — pages, /api, /api/admin, /api/admin/boards[/screens]
      middleware/         — Cache headers
      plugins/
        base.ts           — Plugin types, manifests, schema validation
        index.ts          — Plugin registry (add new plugins here)
        runtime.ts        — Refresh loops and error handling
        datetime/         — Clock
        weather/          — Open-Meteo current conditions and 3-day forecast
        github/           — Repo stats, open issues/PRs
        api-ninjas/       — Quote of the Day, random quotes, crypto prices

Customization

Everything that describes a board — its size, timings, charset, accent colours, corner markers, loading-line mode and transition order — is edited per board in the admin dashboard, under Boards. Changes persist to ~/.flipoff/flipoff.db and reach open displays immediately over the WebSocket, without a reload. A grid resize is the exception: it rebuilds the board, so those displays do reload.

Its screens — the rotation itself — are edited alongside its configuration, at Boards → (a board) → Screens.

The seed

backend/src/config/seed.ts holds what a board is born with: grid, timings, charset, accent colours, corners, loading mode, transition mode and the messages its manual screens start from.

It is read in one place, when a board row is inserted, and nowhere else. Editing it therefore has no effect on an install that already has boards — it changes what the next board you create gets, and what a fresh install's first board gets. There is nothing to bind-mount and no restart-time override: change a running board in the admin instead.

Note that charset drives the flip animation rather than filtering text. Characters outside it, emoji included, still render.

Environment Variables (backend)

Variable Default Description
PORT 8080 Server listen port
ADMIN_PASSWORD Auto-generated Admin dashboard password
API_NINJAS_API_KEY API key for quote and crypto price plugins
FLIPOFF_DATA_DIR ~/.flipoff Where flipoff.db (boards and screens) persists

License

MIT — do whatever you want with it.

About

Free split-flap display emulator for any display.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages