Skip to content
WolfWave
Under the hood

Architecture

How WolfWave is built. MVVM + service-oriented Swift. ScriptingBridge → Apple Music, EventSub WebSocket → Twitch, IPC → Discord, WebSocket → overlay.

WolfWave follows a clean architecture with clear separation of concerns. Pattern: MVVM + Service-Oriented, with an NSApplicationDelegateAdaptor-based lifecycle.

Project Structure

The native app lives at apps/native/WolfWave/ with tests at apps/native/WolfWaveTests/. The top-level layout:

WolfWave/
├── WolfWaveApp.swift              # @main + NSApplicationDelegateAdaptor
├── AppDelegate+*.swift            # MenuBar / Services / Windows splits
├── Core/                          # Constants, Keychain, Logger, StreamerMode, …
├── Monitors/                      # PlaybackSource protocol + AppleMusicSource (ScriptingBridge)
├── Services/
│   ├── Discord/                   # DiscordRPCService. Local IPC socket
│   ├── ListeningHistory/          # Opt-in NDJSON play log + stats + monthly wrap
│   ├── Notifications/             # Opt-in song-change banner
│   ├── SongRequest/               # Queue, resolvers, AppleMusicController, blocklist, vote-skip
│   ├── Twitch/                    # ChatService (EventSub), ChannelPointsService, DeviceAuth, Commands/
│   ├── UpdateChecker/             # SparkleUpdaterService
│   └── WebSocket/                 # Token-gated WebSocketServerService (+ Stream Deck commands) + static WidgetHTTPService
├── Views/                         # SwiftUI settings shell + per-section views + Onboarding wizard
└── Resources/                     # widget.html, Assets.xcassets, dev-appcast.xml

For the full file-by-file breakdown. Which rots every PR. See the Source layout section of CLAUDE.md in the repo root.

Architecture Highlights

MARK Sections

Every file uses clear section markers for easy navigation:

// MARK: - Properties
// MARK: - Initialization
// MARK: - Public Methods
// MARK: - Private Helpers

Documentation

Comprehensive DocC-style comments with parameter/return documentation throughout the codebase.

Delegation Pattern

PlaybackSourceDelegate is used for track update notifications, following Apple's delegation pattern. The delegate receives track name, artist, album, duration, and elapsed time on every update.

MVVM with @Observable

ViewModels separate UI logic from business logic. WolfWave uses the modern @Observable macro (migrated from ObservableObject / @Published):

  • TwitchViewModel manages Twitch connection state.
  • OnboardingViewModel drives the first-run wizard.
  • Views observe @Observable properties directly. No @StateObject wrapper required.

Modern Concurrency

Swift's async/await is used throughout for asynchronous operations:

func fetchData() async throws -> Data {
    // Modern async implementation
}

Loose Coupling via NotificationCenter

Settings changes (e.g. TrackingSettingChanged, DockVisibilityChanged) flow through NotificationCenter. Names centralized in AppConstants.Notifications.

Thread Safety

  • TwitchChatService: NSLock for shared state mutations.
  • DiscordRPCService: serial ipcQueue confinement + enabledLock.
  • Logger: serial DispatchQueue for thread-safe file I/O.
  • WebSocketServerService: separate NSLocks for connections, playback state, and enabled flag.

Key Components

KeychainService

Secure storage for sensitive credentials using the macOS Keychain API. All tokens and secrets are stored securely, never in UserDefaults or plain text. Keys defined in AppConstants.Keychain.

PlaybackSource

Source abstraction backed by AppleMusicSource, which uses ScriptingBridge for direct Apple Music communication without spawning subprocesses. Provides real-time track updates (including duration and elapsed time) via delegation, with a 2-second fallback poll. PlaybackSourceManager selects and multiplexes sources.

TwitchChatService

Full Twitch integration using:

  • Helix API for sending messages and API requests.
  • EventSub WebSocket for real-time chat message notifications.
  • OAuth Device Code Flow for secure authentication.
  • Network path monitoring for automatic reconnection.

SongRequestService

Coordinates chat song requests end-to-end:

  • Queue with hold mode and buffering when Music.app is closed.
  • Resolvers: SongSearchResolver (MusicKit) and LinkResolverService (Apple Music URLs).
  • Playback: AppleMusicController plays tracks via AppleScript while preserving window focus.
  • Blocklist: SongBlocklist blocks by track ID, artist, or album.
  • Approval screening: opt-in "Require My Approval" (isApprovalRequired, approve/decline) holds each request in a pending state until the broadcaster acts, across chat, channel points, and bits.

DiscordRPCService

Discord Rich Presence integration using:

  • Local IPC Socket: Unix domain socket. No bot token or server required.
  • iTunes Search API: Album artwork fetched dynamically with in-memory caching.
  • Playback Progress: Elapsed time and duration shown as a progress bar.
  • Auto-reconnect: Detects Discord availability and reconnects on its own.
  • Sandbox Compatible: Uses SBPL entitlements for socket access within the App Sandbox.

WebSocketServerService

Local WebSocket server for OBS stream overlays using:

  • Network.framework NWListener: Native WebSocket server with auto-ping and multi-client support.
  • JSON Broadcasting: Sends welcome, now_playing, progress, playback_state, overlay_visibility, widget_config, and (for the Stream Deck plugin) queue_state / health messages.
  • Bidirectional control channel: Parses inbound command frames from an authenticated client (the Stream Deck plugin) via an injected onCommand handler and replies with an ack. Envelope decode is pure (StreamDeckControl.parse); the command router lives in AppDelegate+StreamDeck. See the Stream Deck control-API notes in apps/native/docs/streamdeck-control-api.md.
  • Progress Timer: 1-second interval broadcasts elapsed-time estimation to avoid polling ScriptingBridge.
  • Auto-retry: Reconnects the listener after failures with configurable delay.
  • LAN-reachable, token-gated: Binds to all interfaces on :8765 so a second-PC OBS or phone on the same network can connect. Every WebSocket connection, loopback or LAN, must present Sec-WebSocket-Protocol: wolfwave.token.<hex> matching the per-install token in the macOS Keychain. See Security.

Owns a WidgetHTTPService instance that starts and stops alongside it.

WidgetHTTPService

Tiny companion HTTP server that serves the bundled widget.html to OBS:

  • LAN-reachable: Binds to all interfaces on :8766 so remote browsers can pull the widget HTML. The static HTTP response is not authenticated.
  • Credential bootstrap: Responses inject the token only for a loopback TCP peer plus a literal local Host, and mark token-bearing HTML no-store. LAN URLs carry ?token=…; the browser script forwards it in the authenticated WebSocket handshake.
  • GET /200 OK with widget.html bytes from the app bundle.
  • Generated assets: /widget-tokens.generated.js, /favicon.ico, and /favicon.png are also served.
  • All other paths404 Not Found.
  • Lifecycle: Owned by WebSocketServerService; starts/stops with the WS server.

The bundled widget.html is a generated artifact. Its source lives in the apps/widget/ workspace (Tailwind + TypeScript), and the build pipeline inlines the compiled CSS, design tokens, and JS runtime into a single self-contained HTML file. The committed widget.html ships as-is (Xcode does not rebuild it); CI rebuilds it before xcodebuild, and test CI fails the PR on drift between the apps/widget/ sources and committed output. See the OBS Widget Architecture page for the full pipeline, message contract, theme/layout system, and transition state machine.

SparkleUpdaterService

Wraps the Sparkle framework for auto-updates:

  • EdDSA-signed appcast verified against the public key in Info.plist (SUPublicEDKey).
  • Release builds poll the remote SUFeedURL.
  • DEBUG builds disable automatic checks; manual "Check Now" reads the bundled dev-appcast.xml.
  • Homebrew installs disable Sparkle entirely (updates handled by Homebrew).

BotCommandDispatcher

Extensible command routing system that:

  • Registers available commands at startup (registerDefaultCommands()).
  • Merges streamer-authored CustomBotCommands from CustomCommandStore on top of the built-ins, rebuilt per message so edits apply on the next chat line.
  • Matches incoming messages to trigger sets (commands implement BotCommand or AsyncBotCommand).
  • Enforces global + per-user cooldowns via CooldownManager (mods bypass).
  • Returns responses capped at 500 chars, with execution target under 100 ms.

On this page