Birdie

A minimal, privacy-first desktop client for X (Twitter)

Birdie

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.

View on GitHub

Download

macOS

Universal (Intel + Apple Silicon)

.dmg (190 MB)
Windows

NSIS Installer

.exe (84 MB)
Linux

AppImage & .deb

AppImage .deb

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 renderer
  • contextIsolation: true — preload scripts run in an isolated world
  • sandbox: 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, and clipboard-sanitized-write are allowed
  • Domain allowlist — navigation is restricted to 12 known X/Twitter domains; all other URLs open in the system browser

Main Process Modules

ModuleResponsibility
index.tsApp entry point. Enforces single-instance lock, orchestrates startup (theme → window → menu → tray → updater), handles macOS activate lifecycle.
window.tsCreates the BrowserWindow with persisted bounds, sets a Chrome-compatible user agent, installs navigation guards and new-window handlers, wires up permission checks.
tray.tsSystem tray icon with Show/Hide/Quit context menu; click toggles window visibility.
menu.tsNative application menu with platform-aware templates (macOS app menu vs. Windows/Linux). Adds Navigate submenu (Home, Back, Forward) with keyboard accelerators.
store.tsPersistent preferences via electron-store. Stores window bounds (x, y, width, height) and theme preference (system/dark/light).
theme.tsReads stored theme preference and applies it via nativeTheme.themeSource. Provides a toggle function for runtime switching.
updater.tsAuto-update via electron-updater. Checks GitHub Releases on startup (only in packaged builds), downloads and installs on quit.
external-links.tsURL validation against the ALLOWED_DOMAINS allowlist. Supports exact match and subdomain matching.
notifications.tsSession-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-store and restored on next launch, so the app always opens where you left it.
  • Single instance lockapp.requestSingleInstanceLock() prevents multiple Birdie windows. If you launch again, the existing window is focused instead.

Tech Stack

LayerTechnology
RuntimeElectron 35, Node.js, Chromium
LanguageTypeScript 5.8 (strict, separate tsconfigs for main + preload)
Persistenceelectron-store 8 (JSON on disk)
Updateselectron-updater 6 (GitHub Releases provider)
Packagingelectron-builder 25 (DMG, NSIS, AppImage, deb, snap)
TestingVitest (unit/integration), Playwright (E2E)
LintingESLint 9, Prettier 3

Keyboard Shortcuts

ShortcutAction
Cmd/Ctrl + NNew post
Cmd/Ctrl + FSearch
Cmd/Ctrl + ,Settings
Cmd/Ctrl + RRefresh
Cmd/Ctrl + Shift + HHome
Cmd/Ctrl + [Back
Cmd/Ctrl + ]Forward
Cmd/Ctrl + WClose window
Cmd/Ctrl + QQuit

Building from Source

git clone https://github.com/sumanyumuku98/Birdie.git
cd Birdie
npm install
npm run dev
CommandDescription
npm run devBuild and launch the app
npm run buildCompile TypeScript
npm testRun unit + integration tests
npm run test:e2eRun Playwright E2E tests
npm run packageBuild distributable
npm run lintRun ESLint
npm run formatFormat with Prettier