A self-hosted family wallboard — calendar, weather, photos, news, sports, quotes, and more — laid out as Bento tiles you can rearrange like Squarespace from a built-in admin panel. Designed for a wall-mounted display (Raspberry Pi + salvaged monitor works great), but it runs on any computer with Node.js.
- One screen, two layouts. Edit a portrait layout and a landscape layout separately — the wallboard auto-picks whichever matches your screen orientation.
- Drag, drop, and resize tiles on a snapping grid right from the admin panel. No editing JSON unless you want to.
- 17 built-in widgets (calendar, weather, news, sports, photos, quote carousel, NASA picture of the day, on-this-day memories, Bible verse, dinner-conversation questions, and more). Add your own with two small files.
- No accounts, no API keys required out of the box. Everything works on first boot with sensible defaults; you can plug in your own iCloud calendar URL, photo folder, RSS feeds, and sports teams from the admin UI.
- Runs anywhere Node runs — Mac, Linux, Windows, Raspberry Pi.
- Quick start
- Hardware setup
- The admin panel
- Widget catalog
- Personalizing your data
- Adding a custom widget
- Troubleshooting
- Project structure
- Security
You need Node.js 18 or newer installed. On a Mac, the easiest way is to install Homebrew and then run brew install node.
# 1. clone this repository
git clone https://github.com/ShamgarBN/wallboard.git
cd wallboard
# 2. install dependencies (one-time, takes ~30 seconds)
npm install
# 3. start the server
npm startYou'll see:
✓ Wallboard running at http://localhost:3000
✓ Admin at http://localhost:3000/admin
Open http://localhost:3000 in any browser — that's the wallboard.
Open http://localhost:3000/admin to edit it.
If you're running this on a Raspberry Pi (or any other machine on your home network) and want to access the admin from your phone, replace localhost with the Pi's IP address or hostname, e.g. http://wallboard.local:3000/admin.
Parts list (~$110 if you have a spare monitor):
| Part | Approx. cost | Notes |
|---|---|---|
| Raspberry Pi 5 (4 GB) | $60 | Pi 4 also works |
| Official Pi 5 power supply | $12 | Use the official one — knockoffs cause undervolting glitches |
| microSD card 32 GB+ | $10 | Class 10 / U3 minimum |
| micro-HDMI to HDMI cable | $8 | Pi 5 has two micro-HDMI ports |
| Pi case with cooling | $10 | Any well-vented case |
| Monitor | $0 if salvaged | 1080p or higher, any aspect ratio |
| (Optional) PIR motion sensor | $4 | Wires to GPIO for auto-on/off |
| (Optional) Shadowbox frame | $30 | At any craft store — turns a salvaged monitor into wall art |
One-time setup on the Pi:
- Flash Raspberry Pi OS Lite (64-bit) to the microSD card using Raspberry Pi Imager. In the Imager's settings, configure SSH, wifi, your username, and password.
- Boot the Pi and SSH into it:
ssh [email protected](or whatever hostname you chose). - Install Node.js:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs git chromium-browser unclutter xserver-xorg x11-xserver-utils xinit openbox - Clone and run the wallboard:
git clone https://github.com/ShamgarBN/wallboard.git ~/wallboard cd ~/wallboard && npm install
- Run on boot — create a systemd service so the wallboard starts automatically:
sudo tee /etc/systemd/system/wallboard.service > /dev/null <<'EOF' [Unit] Description=Wallboard server After=network.target [Service] Type=simple User=pi WorkingDirectory=/home/pi/wallboard ExecStart=/usr/bin/node server.js Restart=always RestartSec=5 Environment=PORT=3000 [Install] WantedBy=multi-user.target EOF sudo systemctl enable --now wallboard
- Launch a kiosk browser on boot — make Chromium open the wallboard fullscreen with the cursor hidden:
mkdir -p ~/.config/openbox cat > ~/.config/openbox/autostart <<'EOF' xset -dpms # disable energy star xset s off # disable screensaver xset s noblank # don't blank the screen unclutter -idle 0.5 -root & chromium-browser --kiosk --noerrdialogs --disable-infobars --disable-translate \ --no-first-run --check-for-update-interval=31536000 \ --app=http://localhost:3000 & EOF echo "exec openbox-session" > ~/.xinitrc # auto-start X on login echo '[ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ] && startx' >> ~/.bash_profile sudo raspi-config nonint do_boot_behaviour B2 # console autologin sudo reboot
After reboot the Pi should boot straight into the wallboard, full-screen.
Mounting the monitor:
- Pull the monitor's bezel and stand off (most pop or unscrew). Just the bare panel inside a shadowbox frame looks dramatically more premium than the original monitor.
- A 11"×14" shadowbox frame fits most 13"–15" monitors. Cut the mat opening to the screen's exact active area.
- Run the power cable down through the back; the Pi can sit on the back panel held by velcro.
Open http://<your-server>:3000/admin. There are five tabs:
The Bento grid editor:
- Drag tiles to move them. They snap to the grid.
- Drag the bottom-right corner of any tile to resize it (also snaps).
- Click the dot row at the bottom-left of a tile to change its accent color.
- Click the × in the corner to remove a tile.
- Drag a widget from the sidebar onto the grid to add a new tile — or just click it to drop it in the first empty spot.
- Toggle between Portrait and Landscape at the top of the sidebar. Each orientation has its own independent layout.
- Change grid dimensions to make tiles smaller (more columns/rows) or larger (fewer).
- Hit Save layout when done. The wallboard auto-reloads within 60 seconds.
- Household name (shows on the wallboard).
- Default orientation.
- Location for weather and air-quality (label + lat/lon).
- Optional admin password — set this if your Pi is reachable from outside your home network.
Per-widget options like time format (12h/24h), temperature units (°F/°C), Bible translation, etc.
The big "give me my data" tab:
- Calendar URL — paste your iCloud public-share
webcal://URL here. - News feeds — add, remove, or rename RSS feeds.
- Sports teams — pick from MLB, NFL, NBA, NHL, College Football, College Basketball, MLS, WNBA.
- Birthdays — name, month, day. The reminders widget surfaces them as they approach.
- Trash day — pick your recycling/garbage night.
- Dinner questions — one per line; rotates daily.
A plain text editor over your data/quotes.csv file. Format: text,author,tags. The quote widget rotates through these. You can also edit data/quotes.csv directly with any spreadsheet program if you prefer — both work.
| Widget | What it does | Data source |
|---|---|---|
| Date & Time | Big clock with day and date. 12/24h selectable. | Local |
| Weather | Current conditions plus a 3/5/7-day forecast with hi/lo + icons. | Open-Meteo (free, no key) |
| Today's Agenda | Today's events from your iCal/iCloud calendar. | Public iCal URL |
| This Week | Next 7 days, grouped by day. | Same iCal URL |
| News Headlines | Round-robin headlines from your RSS feeds. | Any RSS |
| Quote | Rotating quote carousel from your CSV. | data/quotes.csv |
| Sports | Last + next game for each followed team. | ESPN public JSON |
| Photo Slideshow | Rotating photos from data/photos/. |
Local folder |
| Reminders | Upcoming birthdays + trash day. | Admin-configured |
| Moon Phase | Current phase + % illumination. | Computed locally |
| NASA Picture of the Day | Today's APOD image + caption. | NASA APOD API (no key) |
| On This Day | Photo from the same date in past years. | data/photos/YYYY-MM-DD-*.jpg |
| Word of the Day | New vocabulary word daily. | Curated local list |
| Dinner Question | Conversation starter, rotates daily. | Admin-configured |
| Weekly Verse | Bible verse, rotates weekly. | bible-api.com (free, no key) |
| Air Quality | AQI + UV index for your location. | Open-Meteo Air Quality |
To pipe your Apple Calendar to the wallboard:
- Open Calendar.app on your Mac (works on iPhone too but the Mac flow is clearer).
- Right-click a calendar in the sidebar → Share Calendar.
- Check Public Calendar. A
webcal://URL appears. - Copy that URL, paste it into the admin → Sources → iCal / Webcal URL field, hit Save.
Only the calendars you explicitly make public will be visible. The URL is opaque (unguessable) but treat it like a password — anyone with the URL can read events you've shared (not write).
Works with Google Calendar (use the "Secret iCal address" from a calendar's settings), Outlook/Office 365, Fastmail, ProtonMail, Nextcloud — anything that produces an iCal/webcal URL.
Drop image files (.jpg, .png, .webp, .gif) into data/photos/. The Photo Slideshow widget will rotate through them.
For the On This Day widget, name files like YYYY-MM-DD-anything.jpg (e.g. 2022-05-21-avery-birthday.jpg) and the widget will surface them on the matching date.
iCloud Shared Album support is on the roadmap (Phase 2.5) since Apple's photo-sharing API is undocumented and finicky. In the meantime, on a Mac you can subscribe to an iCloud shared album, drag-export new photos to a synced folder (e.g. via Hazel or a launchd job), and rsync them to the Pi. Detailed guide coming.
Add any RSS feed via admin → Sources → News feeds. Some great defaults:
- NYT
https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml - BBC World
http://feeds.bbci.co.uk/news/world/rss.xml - NPR
https://feeds.npr.org/1001/rss.xml - Guardian World
https://www.theguardian.com/world/rss - Hacker News
https://hnrss.org/frontpage - ESPN
https://www.espn.com/espn/rss/news - Your local paper — most newspapers have an
/rssor/feedURL hiding somewhere.
Pre-configured for the Niemann family:
- Atlanta Braves (MLB)
- Carolina Panthers (NFL)
- Georgia Bulldogs (NCAA Football)
- Carolina Hurricanes (NHL)
Add or change in admin → Sources → Sports teams. Each team needs a name (e.g. "Atlanta Braves"), abbreviation (e.g. "ATL"), and league.
Supported leagues: MLB, NFL, NBA, NHL, College Football, College Basketball (Men's), MLS, WNBA.
Edit data/quotes.csv directly (a spreadsheet program handles this cleanly) or use the Quotes tab in admin. Format:
text,author,tags
"Be still, and know that I am God.",Psalm 46:10,scripture
"It does not do to dwell on dreams and forget to live.",J.R.R. Tolkien,literaryIf your text contains a comma, wrap it in double quotes. Tags are optional and currently informational only (future versions may filter by tag).
In admin → General → Location:
- Easiest: type a city name in City label for display purposes, then leave lat/lon. The system will auto-detect.
- Most accurate: open Google Maps, right-click your house, click the coordinates that appear at the top to copy them, paste into Lat/Lon fields.
A widget is two small files. Say you want a "Spotify Now Playing" widget:
1. Server-side fetcher — lib/widgets/spotify.js
const cache = require('../cache');
module.exports = {
meta: {
name: 'Spotify Now Playing',
description: 'What\'s playing right now.',
category: 'Media',
defaultSize: { colspan: 2, rowspan: 1 },
defaultAccent: 'green',
settings: [
{ key: 'username', type: 'text', label: 'Spotify username' },
],
},
async fetch(config) {
// ... fetch from Spotify API, return JSON ...
return { track: 'Hey Jude', artist: 'The Beatles' };
},
};Register it in lib/widgets/registry.js:
const spotify = require('./spotify');
// ...
const widgets = {
// ...
spotify,
};2. Client-side renderer — public/widgets/spotify.js
(function () {
window.WIDGETS = window.WIDGETS || {};
window.WIDGETS.spotify = {
meta: { title: 'Now Playing' },
render(body, data) {
body.innerHTML = '<div>' + data.track + ' — ' + data.artist + '</div>';
},
};
})();Add <script src="/widgets/spotify.js"></script> to public/index.html.
Restart the server. The widget appears in the admin's library, draggable onto the grid.
"The wallboard says 'Cannot reach server.'"
The server probably isn't running. SSH into the Pi: sudo systemctl status wallboard. Restart: sudo systemctl restart wallboard.
"Weather shows 'Error' or wrong city." Check admin → General → Location. The lat/lon must be numbers. If they're empty, weather defaults to Nashville.
"Calendar is empty."
Three things to check: (1) you pasted the right webcal:// URL in admin → Sources, (2) the iCloud calendar is actually marked Public, (3) you actually have events today (try the Week view to confirm).
"News only shows one source."
The widget round-robins by recency. If a feed isn't returning items, check server logs: sudo journalctl -u wallboard -n 50. Possible causes: feed URL is wrong, the source is rate-limiting you, your Pi can't reach the internet.
"Sports widget shows 'No teams.'" Add teams in admin → Sources → Sports teams. The abbreviation matters — ESPN matches by abbreviation first.
"Tile won't drag on touchscreen." Use a mouse for now — touch support for the editor is a known gap (it's on the list).
"How do I reset everything?"
Delete data/config.json and data/layout.json and restart. The defaults from data/defaults/ repopulate.
wallboard/
├── server.js # Express server entry point
├── package.json
├── README.md
│
├── lib/
│ ├── config.js # Loads and saves config + layout JSON
│ ├── cache.js # Tiny in-memory cache for API responses
│ └── widgets/ # Server-side widget data fetchers
│ ├── registry.js # Widget registry
│ ├── datetime.js
│ ├── weather.js
│ ├── agenda-today.js
│ ├── agenda-week.js
│ ├── news.js
│ ├── quote.js
│ ├── sports.js
│ ├── photo.js
│ ├── reminders.js
│ ├── moon.js
│ ├── apod.js
│ ├── on-this-day.js
│ ├── word-of-day.js
│ ├── dinner-question.js
│ ├── verse.js
│ └── air-quality.js
│
├── public/ # Browser code
│ ├── index.html # The wallboard itself
│ ├── styles/wallboard.css
│ ├── scripts/
│ │ ├── wallboard.js # Layout renderer + refresh loop
│ │ └── icons.js # Inline SVG icon set
│ ├── widgets/ # Client-side widget renderers (one per widget)
│ └── admin/
│ ├── index.html # Admin panel UI
│ ├── admin.css
│ ├── admin.js # Main admin controller
│ └── editor.js # Drag-and-drop layout editor
│
├── data/ # Local data (gitignored)
│ ├── config.json # Generated on first boot
│ ├── layout.json # Generated on first boot
│ ├── quotes.csv # Curated, you edit this
│ ├── photos/ # Drop your photos here
│ └── defaults/ # Seed defaults shipped with the repo
│ ├── config.json
│ └── layout.json
│
└── mockups/ # Phase 1 design picker (kept for reference)
This is a home-LAN tool, but it follows defensive defaults:
- HTTP-only Basic auth on
/adminif you set a password (optional, off by default). - Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy headers set on every response.
- Cache-Control: no-store, so admin tokens don't leak through caches.
- Photo serving uses
path.basename()and rejects directory-traversal attempts. - The API never reflects the admin password; it's only ever set, never read back to the browser.
- All third-party data (RSS, ESPN, weather) is treated as untrusted and rendered with HTML escaping.
If you expose the wallboard outside your home network (via Cloudflare Tunnel, Tailscale, etc.):
- Set an admin password in admin → General → Admin password.
- Run behind HTTPS (Tailscale, Caddy, Cloudflare all handle this for free).
- Consider restricting access to the admin route via your reverse proxy.
MIT. See LICENSE.
