Birdie
A minimal, privacy-first desktop client for X (Twitter)
Birdie is a lightweight Electron-based desktop wrapper for X (formerly Twitter). No bloat, no custom UI — just X in a native window with system tray integration, keyboard shortcuts, notifications, auto-updates, and dark mode support. Zero telemetry, zero tracking, zero injected code.
Download
Install via Package Managers
# macOS (Homebrew)
brew install --cask sumanyumuku98/birdie/birdie
# Arch Linux (AUR)
yay -S birdie-bin
Architecture
Birdie follows Electron's recommended multi-process architecture with strict security boundaries. The app is split into three layers — Main, Preload, and Renderer — each running in its own process with minimal surface area between them.
Birdie Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Main Process (Node.js — full system access) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ index.ts │ │ window.ts│ │ tray.ts │ │ menu.ts │ │
│ │ App boot │ │ BWindow │ │ System tray│ │ Native menus │ │
│ │ Lifecycle│ │ creation │ │ icon & │ │ & keyboard │ │
│ │ & single │ │ nav guard│ │ context │ │ shortcuts │ │
│ │ instance │ │ UA spoof │ │ menu │ │ (Cmd+N/F/R/W/Q) │ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ theme.ts │ │ store.ts │ │updater.ts │ │ external-links.ts│ │
│ │ System │ │ Electron │ │ Auto-update│ │ Domain allowlist │ │
│ │ dark/ │ │ Store │ │ via GitHub │ │ navigation guard │ │
│ │ light │ │ persist │ │ Releases │ │ (x.com, t.co...) │ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ notifications.ts │ │
│ │ Permission handler — whitelist: notifications, │ │
│ │ media, clipboard-read, clipboard-sanitized-write │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────────┘
│
contextBridge (IPC)
┌─────────┴─────────┐
│ Preload Process │
│ preload/index.ts │
│ │
│ Exposes minimal │
│ API: version only │
│ via contextBridge │
└─────────┬─────────┘
│
contextIsolation: true
nodeIntegration: false
sandbox: true
│
┌─────────┴──────────┐
│ Renderer Process │
│ │
│ x.com loaded in a │
│ fully sandboxed │
│ BrowserWindow │
│ │
│ No IPC bridge to │
│ renderer — X runs │
│ in total isolation │
└─────────────────────┘
Request & Navigation Flow
Navigation Guard & External Link Handling
User clicks link in X
│
▼
┌───────────────┐ Yes ┌────────────────────┐
│ will-navigate │───────────▶│ isInternalURL(url) │
│ event fired │ │ checks ALLOWED_ │
└───────────────┘ │ DOMAINS list │
└────────┬───────────┘
│ │
Yes No
│ │
▼ ▼
┌──────────┐ ┌─────────────────┐
│ Load in │ │ event.prevent() │
│ same │ │ shell.open │
│ window │ │ External(url) │
└──────────┘ │ → system browser │
└─────────────────┘
Allowed Domains: x.com, twitter.com, twimg.com, t.co,
abs.twimg.com, pbs.twimg.com, video.twimg.com,
ton.twimg.com, api.x.com, api.twitter.com,
upload.twitter.com, caps.twitter.com
Build & Distribution Pipeline
CI/CD & Packaging Pipeline
TypeScript Source
(src/main, src/preload, src/shared)
│
▼
┌────────────────┐ ┌──────────────────┐
│ tsc compile │────▶│ dist/main/ │
│ 2 tsconfigs: │ │ dist/preload/ │
│ main + preload│ │ (compiled JS) │
└────────────────┘ └────────┬─────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌────────────────┐ ┌────────────────┐
│ electron- │ │ electron- │ │ electron- │
│ builder │ │ builder │ │ builder │
│ --mac │ │ --win │ │ --linux │
│ │ │ │ │ │
│ Universal DMG │ │ NSIS .exe │ │ AppImage │
│ (Intel + ARM) │ │ installer │ │ .deb / .snap │
└───────┬───────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
└─────────────────────┼───────────────────────┘
│
▼
┌───────────────────────┐
│ GitHub Releases │
│ + electron-updater │
│ (auto-update OTA) │
└───────────────────────┘
Technical Deep Dive
Security Model
Birdie follows Electron's strictest security recommendations. The renderer process (which loads x.com) runs in a fully sandboxed environment with:
nodeIntegration: false— no Node.js APIs in the renderercontextIsolation: true— preload scripts run in an isolated worldsandbox: true— OS-level sandboxing via Chromium- No IPC bridge — the preload script only exposes the app version string; x.com has zero access to system resources
- Permission whitelisting — only
notifications,media,clipboard-read, andclipboard-sanitized-writeare allowed - Domain allowlist — navigation is restricted to 12 known X/Twitter domains; all other URLs open in the system browser
Main Process Modules
| Module | Responsibility |
|---|---|
index.ts | App entry point. Enforces single-instance lock, orchestrates startup (theme → window → menu → tray → updater), handles macOS activate lifecycle. |
window.ts | Creates the BrowserWindow with persisted bounds, sets a Chrome-compatible user agent, installs navigation guards and new-window handlers, wires up permission checks. |
tray.ts | System tray icon with Show/Hide/Quit context menu; click toggles window visibility. |
menu.ts | Native application menu with platform-aware templates (macOS app menu vs. Windows/Linux). Adds Navigate submenu (Home, Back, Forward) with keyboard accelerators. |
store.ts | Persistent preferences via electron-store. Stores window bounds (x, y, width, height) and theme preference (system/dark/light). |
theme.ts | Reads stored theme preference and applies it via nativeTheme.themeSource. Provides a toggle function for runtime switching. |
updater.ts | Auto-update via electron-updater. Checks GitHub Releases on startup (only in packaged builds), downloads and installs on quit. |
external-links.ts | URL validation against the ALLOWED_DOMAINS allowlist. Supports exact match and subdomain matching. |
notifications.ts | Session-level permission handler. Whitelists specific permissions, denies everything else. |
Key Design Decisions
- Zero custom renderer — Birdie loads x.com directly rather than building a custom client. This means zero maintenance burden when X changes its UI/API, and users get the exact same experience as the website.
- Minimal preload surface — The preload script only exposes
window.birdie.version. There is no IPC channel between main and renderer, drastically reducing the attack surface. - User-agent spoofing — The window uses a standard Chrome user agent to prevent X from serving a degraded "unsupported browser" experience.
- Persistent window state — Window position and size are saved to
electron-storeand restored on next launch, so the app always opens where you left it. - Single instance lock —
app.requestSingleInstanceLock()prevents multiple Birdie windows. If you launch again, the existing window is focused instead.
Tech Stack
| Layer | Technology |
|---|---|
| Runtime | Electron 35, Node.js, Chromium |
| Language | TypeScript 5.8 (strict, separate tsconfigs for main + preload) |
| Persistence | electron-store 8 (JSON on disk) |
| Updates | electron-updater 6 (GitHub Releases provider) |
| Packaging | electron-builder 25 (DMG, NSIS, AppImage, deb, snap) |
| Testing | Vitest (unit/integration), Playwright (E2E) |
| Linting | ESLint 9, Prettier 3 |
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| Cmd/Ctrl + N | New post |
| Cmd/Ctrl + F | Search |
| Cmd/Ctrl + , | Settings |
| Cmd/Ctrl + R | Refresh |
| Cmd/Ctrl + Shift + H | Home |
| Cmd/Ctrl + [ | Back |
| Cmd/Ctrl + ] | Forward |
| Cmd/Ctrl + W | Close window |
| Cmd/Ctrl + Q | Quit |
Building from Source
git clone https://github.com/sumanyumuku98/Birdie.git
cd Birdie
npm install
npm run dev
| Command | Description |
|---|---|
npm run dev | Build and launch the app |
npm run build | Compile TypeScript |
npm test | Run unit + integration tests |
npm run test:e2e | Run Playwright E2E tests |
npm run package | Build distributable |
npm run lint | Run ESLint |
npm run format | Format with Prettier |