Nishboard is a frameless Electron + React app built to run all day on a secondary monitor. It glances at the things you keep half-watching โ the weather, what's playing on Spotify, your watchlist, CPU/GPU load, who's live on Twitch โ without you ever alt-tabbing. Windows is the primary target, macOS the secondary. It's a personal project, not a product.
- โจ Features
- ๐งฑ Tech Stack
- ๐๏ธ Monorepo Layout
- ๐๏ธ Architecture
- ๐ Getting Started
- โ๏ธ Configuration
- ๐ Scripts
- ๐ฆ Building & Distributing
- ๐ค CI & Releases
- ๐งญ Conventions
โ ๏ธ Gotchas- ๐ Project Structure
- ๐ License
| Widget | What it shows | Data source |
|---|---|---|
| ๐ค๏ธ Weather | Current conditions, hourly strip, 5-day forecast, severe-weather alerts | Open-Meteo + NWS alerts (location via IP, or a ZIP override) |
| ๐ง Spotify | Now playing, transport controls, โฅ save/unsave, playlists + Liked Songs + Recently Played, search, devices | Spotify Web API (PKCE OAuth) โ remote controlยน |
| ๐ Stocks | Editable watchlist, % change, sparklines, market session; click a ticker for an intraday/daily chart + headlines | Alpaca Markets IEX (REST snapshots + Benzinga news) |
| ๐งฎ Hardware | CPU / GPU / RAM / disk / network as bars or live sparklines | systeminformation (nvidia-smi / WASAPI under the hood) |
| ๐ Sound | Master volume, mute, output switching, per-app mixer (Windows) | osascript (mac) / PowerShell + WASAPI (Windows) |
| ๐ Calendar | Month grid + Google Calendar: event dots, click a day for its events, inline quick-add | Google Calendar API (shares the YouTube OAuth client) |
| Search, Trending/Music/Gaming, and โ signed in โ your Subs feed, Playlists, Liked; click any channel name for its uploads; watch inline; optionally hide Shorts across every tab, or switch the Subs tab to a plain channel list (click a channel for its uploads) โ both in Settings โ App | YouTube Data API v3 + Google OAuth + localhost embed proxy | |
| ๐ฃ Twitch | Search channels; signed in: Live + All followed channels, go-live notifications; watch inline | Twitch Helix (app token + user OAuth) + localhost embed proxy |
| ๐ฐ News | Rotating headline ticker, click to open the article | Google News RSS โ no key |
| ๐ Notes | Markdown scratchpad, edit โ rendered toggle | Local (localStorage) |
| โ Tasks | Quick checklist โ add, check off, clear completed | Local (localStorage) |
| ๐ World Clock | Multiple timezones, digital or analog faces | Pure JS โ no API |
| โฑ๏ธ Timer & Alarm | Countdown timers + wall-clock alarms with a native notification | Local + notification IPC |
| ๐ฏ Countdown | Days/hours remaining to a target datetime | Local (localStorage) |
| ๐ช Crypto | Coin watchlist โ price, 24h change, 7-day sparkline | CoinGecko (optional key) |
| ๐ Launcher | App + link launcher with groups, launch-all, real icons | Local (typed IPC, targets stored main-side) |
| ๐ Clipboard | Rolling text-clipboard history (in-memory, never persisted) | Electron clipboard poller (only while visible) |
| ๐ถ Net Monitor | Per-host latency / jitter / packet-loss + up/down throughput sparklines | System ping via a server-side sampler + systeminformation |
| ๐ค Claude | Chat with Claude โ streaming replies, GFM markdown, multi-turn context, thinking blocks, animated status shimmer. Interactive prompts: in Ask mode writes/commands pop Allow/Deny cards (reads run freely), the model can ask you multiple-choice questions, and Plan mode ends with an Approve-plan card โ with a chime + native notification when Claude blocks on you. Composer: Ask / Auto / Plan switch, model picker + effort slider, / slash & skill autocomplete, usage popover (5h session + weekly + context meter). Workspace + extra folders configurable in Settings โ Claude |
Your installed Claude Code CLI (bills your claude.ai plan, not API keys) |
| ๐ฌ Discord | The real Discord, embedded whole โ servers, DMs & group chats, replying, voice channels, and screen sharing with an in-widget screen/window picker (system audio shared on Windows). Log in once inside the widget and the session persists โ switching layouts, collapsing, or unpinning never logs you out or drops a voice call (the embed lives for the whole app session); mention-count badge from the tab title; header actions for Home/Reload/Sign out. Native mode (header toggle, persisted): drives your RUNNING Discord desktop app over its local RPC โ browse servers & channels, a read-only live chat feed, join/leave voice channels, mute/deafen, see who's in voice with green speaking rings, and a "Reply in Discord" button that jumps the desktop client to the channel | Embed: Discord web app in an Electron <webview> (own persist:discord session) โ no keys, no bot, no API setup. Native: your own Discord application's client id/secret + the desktop app running |
Everything lives on a draggable, resizable grid (react-grid-layout) with built-in presets, saveable custom layouts โ either kind pinnable to the titlebar for one-click switching โ and 15 themes plus a custom theme editor. The Widgets menu groups every widget into categories (Media / Feeds / Productivity / System / AI) with hover arrows to reorder rows within a category, and any widget can be disabled entirely from Settings โ App โ Widgets โ it vanishes from the menu, the grid, and the command palette until re-enabled. Any widget collapses accordion-style to just its title bar (chevron in the header) and the state persists across restarts โ collapsing hands its space to the widget directly below and expanding takes it back, so the rest of the grid stays exactly where it was. The Ctrl/Cmd+K command palette understands typed commands with arguments โ timer 1h5m3s tea, alarm 7:30am, task buy milk, volume 40, ticker AAPL, zone tokyo โ alongside fuzzy search over every action.
ยน Spotify is a remote control, not a player โ stock Electron has no Widevine CDM, so it can't decode DRM audio. It drives playback on a device you already have open (phone, desktop app, etc.).
| Layer | Tech |
|---|---|
| Shell | Electron 33 (frameless), TypeScript main process |
| Frontend | React 18 ยท Vite ยท Tailwind CSS ยท shadcn/ui ยท Zustand ยท TanStack Query v5 ยท Recharts |
| Backend | Fastify + TypeScript, spawned as a child process on localhost:7432 |
| Monorepo | Turborepo + pnpm workspaces |
| Language | TypeScript everywhere, strict mode โ no any, no untyped casts |
Four workspaces, each with a single responsibility:
| Package | Responsibility |
|---|---|
apps/main |
Electron main process โ BrowserWindow, preload/contextBridge, IPC handlers, spawning + restarting the Fastify server |
apps/renderer |
The React UI โ widgets, stores, the grid, theming |
packages/server |
Fastify API on :7432 โ the only place that talks to external APIs or touches secrets |
packages/shared |
Shared TypeScript types โ the single source of truth, imported by everyone, never redefined |
In dev, Vite and the
tscpath aliases point@dash/sharedstraight at its TypeScript source, so there's no separate build step for shared types while iterating.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Electron main ยท apps/main โ
โ frameless window ยท spawns Fastify ยท typed IPC host โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ loads the renderer
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Renderer ยท apps/renderer โ
โ React + TanStack Query ยท apiClient.get / post โ
โ talks to main via typed window.electron.* โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP ยท localhost:7432
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Fastify server ยท packages/server โ
โ the ONLY layer that calls external APIs / holds secrets โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
Open-Meteo ยท Spotify ยท Alpaca ยท YouTube ยท Twitch ยท OS audio
Rules that never bend:
- The renderer never calls an external API or sees a secret directly โ everything routes through Fastify.
- Renderer โ main is typed contextBridge IPC only (
window.electron.*), never rawipcRenderer.
Where secrets live (checked in this order at runtime):
- safeStorage โ user-entered keys are encrypted with Electron
safeStorageinuserData/credentials.json, decrypted on launch, and injected as env vars into the spawned server. Saving in Settings restarts the server. - Build-time baked values โ
packages/server/build.mjsbakes.envvalues into the server bundle at package time as a single esbuild-defined JSON blob (BUILTINS_JSON). The values land in the compiled bundle only โ never in source or git. This is what lets a distributed DMG/EXE "just work". .envโ loaded only in localpnpm dev. Gitignored.
User OAuth tokens (Spotify, Twitch, YouTube) are stored separately as plain JSON at ~/.dash/<service>_tokens.json (home dir, survives reinstalls).
Embed proxy โ the packaged app loads from file://, which breaks origin checks. YouTube/Twitch players are served from http://localhost:7432/api/<svc>/embed, giving the iframe a valid HTTP parent origin.
Prerequisites
- Node.js โฅ 20
- pnpm 11.3.0 โ
corepack enable && corepack prepare pnpm@11.3.0 --activate
Install & run
git clone <repo-url>
cd nishboard
pnpm install
cp .env.example .env # then fill in your API keys (see Configuration)
pnpm dev # builds shared types, then runs everythingpnpm dev builds @dash/shared, then launches the server, renderer, and Electron together (color-coded output). The window opens automatically once the Vite dev server and Fastify are up.
All keys are optional to start the app โ each widget degrades gracefully if its key is missing. Copy .env.example โ .env and fill what you want.
| Variable | Used by | Required? | Where to get it |
|---|---|---|---|
SPOTIFY_CLIENT_ID |
Spotify | for Spotify | developer.spotify.com โ create an app |
SPOTIFY_CLIENT_SECRET |
Spotify | optional (PKCE doesn't need it) | same app |
SPOTIFY_REDIRECT_URI |
Spotify | yes (exact match) | set to http://127.0.0.1:7432/api/spotify/callback and register that exact URI in the Spotify app |
ALPACA_API_KEY / ALPACA_API_SECRET |
Stocks | for Stocks | alpaca.markets โ free IEX data keys |
ALPACA_BASE_URL |
Stocks | preset | https://data.alpaca.markets/v2 |
YOUTUBE_API_KEY |
YouTube | for search/browse | Google Cloud Console โ YouTube Data API v3 |
YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET |
YouTube + Calendar | for the signed-in tabs / calendar events | Google Cloud Console โ OAuth client (Web application), redirects http://localhost:7432/api/youtube/callback and http://localhost:7432/api/calendar/callback; enable the YouTube Data + Google Calendar APIsยน |
TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET |
Twitch | for Twitch | dev.twitch.tv/console โ register an app, redirect http://localhost:7432/api/twitch/callbackยฒ |
COINGECKO_API_KEY |
Crypto | optional | coingecko.com demo key โ keyless works but throttled |
GITHUB_TOKEN |
update check | optional | fine-grained PAT, read-only Contents โ only needed while the repo is private |
CLAUDE_CODE_OAUTH_TOKEN |
Claude | optional | only if Claude Code isn't logged in on the machine โ claude setup-token. Never baked into builds (personal account token); the widget otherwise just uses your local claude /login session |
SERVER_PORT |
server | preset | 7432 |
Spotify redirect URI is exact-match. Spotify's form rejects
localhostfor some apps, so this project uses the127.0.0.1form โ whatever you put in.envmust match the dashboard registration character-for-character.ยน Publish the Google OAuth consent screen to In production (unverified is fine for personal use) โ apps left in Testing get refresh tokens that expire every 7 days.
ยฒ Twitch search uses an app token; the Following tabs use user OAuth via that redirect. Twitch only permits
httpfor the literal hostlocalhost(not127.0.0.1).
Two ways to supply keys: enter them in Settings โ Developer at runtime (encrypted via safeStorage), or bake them at package time from .env so a distributed build needs no setup. See Architecture.
Run from the repo root:
| Command | What it does |
|---|---|
pnpm dev |
Builds @dash/shared, then runs shared + server + renderer + main concurrently (color-coded). Your everyday command. |
pnpm build |
turbo build across every package (type-checks + bundles). |
pnpm package |
build โ scripts/prepare-wincodesign.cjs โ electron-builder. Produces a distributable DMG/EXE (unsigned, CSC_IDENTITY_AUTO_DISCOVERY=false). |
pnpm typecheck |
turbo typecheck โ tsc --noEmit everywhere. |
pnpm lint |
turbo lint. |
Per-workspace (handy when iterating on one piece):
pnpm --filter @dash/server dev # tsx watch โ Fastify only
pnpm --filter @dash/renderer dev # vite โ UI only
pnpm --filter @dash/main dev # tsc-watch โ electron
pnpm --filter @dash/shared dev # tsc --watch โ rebuild shared typespnpm packageArtifacts land in release/. Both targets are unsigned (no code-signing certs).
Build archive: every locally-packaged installer is uploaded to a Google Drive builds folder via an rclone remote (
nishboard-builds) โrclone copy "release/<installer>" nishboard-builds: -P. One-timerclone config(Google OAuth) sets up the remote.
- Output:
release/Nishboard-0.1.0-arm64.dmg(APFS, arm64). - First launch is Gatekeeper-blocked because it's unsigned โ right-click the app โ Open (one-time).
- Output:
release/Nishboard Setup 0.1.0.exe(NSIS installer). - SmartScreen will warn on an unsigned installer: "Windows protected your PC" โ More info โ Run anyway.
- Non-admin builds are supported.
electron-buildernormally extractswinCodeSign(which contains macOS dylib symlinks), and creating symlinks on Windows needs admin/Developer Mode โ so the build would abort for a normal user.scripts/prepare-wincodesign.cjsruns first and extracts the archive with-xr!darwin, skipping the symlink entries entirely.
asar: falseโ the Fastify server is spawned from disk as a child process and can't run from inside an.asararchive, so app files stay unpacked.electronVersionis pinned inelectron-builder.ymlbecause Electron lives inapps/main/node_modules(a workspace), not the repo root, where the builder would otherwise look.
Two GitHub Actions workflows (.github/workflows/):
| Workflow | Trigger | What it does |
|---|---|---|
CI (ci.yml) |
every PR + push to master | pnpm typecheck + lint + full turbo build |
Release (release.yml) |
every merge to master | derives the semver bump from the PR title, tags vX.Y.Z (tag-only โ master is protected; the tag, not package.json, is the version source of truth), builds the macOS DMG + Windows EXE, publishes a GitHub Release |
Versioning is fully automated โ never bump by hand. The squash-commit subject (= PR title) picks the bump:
| PR title | Bump |
|---|---|
feat!: โฆ / any <type>!: / BREAKING CHANGE in body |
major |
feat: โฆ |
minor |
fix: / chore: / docs: / refactor: / anything else |
patch |
contains [skip release] |
no release |
Manual/off-cycle release: Actions โ Release โ Run workflow (choose the bump). Hand-pushed tags no longer trigger anything.
CI-built installers carry no baked API keys (there's no .env in CI) โ enter keys once in Settings โ Developer. Locally-built (pnpm package) installers bake whatever is in your .env. The in-app update check (Settings โ About) polls the latest GitHub Release and offers a direct download of the right installer for your platform.
- Branch first.
git checkout -b <branch>before touching any files โ never editmasterthen branch. - Naming:
feat/<slug>ยทfix/<slug>ยทchore/<slug>ยทdocs/<slug>. - Update
CHANGELOG.mdbefore every PR. One section per PR, newest first, following the canonical## [PR #N] type: descriptionformat (see CLAUDE.md โ Changelog Format). - No auto-merge. Open the PR and stop โ wait for an explicit "merge".
- PR titles are load-bearing โ they pick the release bump (see CI & Releases). Keep the
<type>: descriptionformat exact. - Strict TypeScript, named exports,
PascalCasecomponents,use*hooks,*Storestores,*DataAPI types, kebab API routes.
- Spotify Development Mode caps at 25 allowlisted users โ a non-allowlisted account gets a 403. Add accounts under your Spotify app โ User Management.
- Stale Spotify token after a client_id change โ refresh tokens are bound to the client_id that minted them; if it changes, hit Disconnect โ Connect (or delete
~/.dash/spotify_tokens.json). - YouTube needs the Electron UA stripped (done automatically) plus the localhost embed proxy; the Data API free tier is ~100 searches/day.
- macOS GPU usage isn't available on Apple Silicon via
systeminformation; audio device switching needsbrew install switchaudio-osx. - Windows audio device switching needs
Install-Module AudioDeviceCmdlets(volume/mute work without it via WASAPI). - Alpaca IEX covers US equities only โ no futures or crypto.
nishboard/
โโโ apps/
โ โโโ main/ # Electron main: BrowserWindow, preload, IPC, server spawn
โ โ โโโ src/
โ โ โโโ index.ts # window + session setup
โ โ โโโ preload.ts # contextBridge API
โ โ โโโ ipc/ # app:* and spotify:* handlers
โ โ โโโ credentials.ts # safeStorage read/write
โ โ โโโ server/spawn.ts # spawn + killStaleOnPort
โ โโโ renderer/ # React UI
โ โโโ src/
โ โโโ components/ # Titlebar, DashboardGrid, WidgetShell, SettingsModal
โ โโโ widgets/ # weather, spotify, stocks, hardware, sound, calendar, youtube, twitch, news, notes, tasks, worldclock, timer, countdown, crypto, launcher, clipboard, claude, discord, embed (shared search+player frame)
โ โโโ store/ # Zustand stores (layout, theme, โฆ)
โ โโโ lib/ # layouts.ts (grid engine), apiClient, utils
โ โโโ index.css # theme tokens + global styles
โโโ packages/
โ โโโ server/ # Fastify API on :7432
โ โ โโโ src/routes/ # one file per widget
โ โ โโโ build.mjs # esbuild bundle + build-time key baking
โ โโโ shared/ # shared TypeScript types
โโโ .github/workflows/ # ci.yml (checks) + release.yml (auto semver + installers)
โโโ build/ # app icons
โโโ scripts/ # prepare-wincodesign.cjs, bump-version.mjs
โโโ electron-builder.yml # packaging config (DMG + NSIS)
โโโ turbo.json # build pipeline
โโโ CLAUDE.md # project + workflow instructions
โโโ CHANGELOG.md # per-PR history
Personal use. Built by and for Nish โ not currently licensed for redistribution.