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/ # WebSocketServerService + WidgetHTTPService (token-gated)
├── Views/ # SwiftUI settings shell + per-section views + Onboarding wizard
└── Resources/ # widget.html, Assets.xcassets, dev-appcast.xmlFor 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 HelpersDocumentation
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):
TwitchViewModelmanages Twitch connection state.OnboardingViewModeldrives the first-run wizard.- Views observe
@Observableproperties directly. No@StateObjectwrapper 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:NSLockfor shared state mutations.DiscordRPCService: serialipcQueueconfinement +enabledLock.Logger: serialDispatchQueuefor thread-safe file I/O.WebSocketServerService: separateNSLocks 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) andLinkResolverService(Apple Music URLs). - Playback:
AppleMusicControllerplays tracks via AppleScript while preserving window focus. - Blocklist:
SongBlocklistblocks 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, andplayback_statemessages. - 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
:8765so a second-PC OBS or phone on the same network can connect. Every accepted connection. Loopback and LAN. Must presentSec-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
:8766so 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 OKwithwidget.htmlbytes from the app bundle.- All other requests →
404 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
BotCommandorAsyncBotCommand). - Enforces global + per-user cooldowns via
CooldownManager(mods bypass). - Returns responses capped at 500 chars, with execution target under 100 ms.
Contribute
Set up Xcode 16, run the test suite, regenerate design tokens, and ship a PR to WolfWave. The open-source Apple Music streamer app.
OBS Widget
How WolfWave's now-playing OBS overlay is built. A Tailwind + TypeScript workspace that compiles into a single self-contained widget.html with smooth play/stop transitions.