Skip to content
WolfWave

Architecture

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

Under the hood

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/                 # WebSocketServerService + WidgetHTTPService (token-gated)
├── 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.

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 automatically.
  • 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, and playback_state messages.
  • 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 accepted connection. Loopback and 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. Same token-gating rules as the WS listener. Loopback peers get the token auto-injected, remote browsers must supply ?token=….
  • GET /200 OK with widget.html bytes from the app bundle.
  • All other requests404 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. Xcode rebuilds it via a pre-build script phase; CI rebuilds it before every xcodebuild invocation. 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()).
  • 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