Sparkle Updates with Stats.store Integration
This document provides comprehensive documentation for VibeTunnel’s automatic update system using Sparkle framework and Stats.store.Overview
VibeTunnel uses a sophisticated update system that combines:- Sparkle Framework - Industry-standard macOS update framework for automatic updates
- Stats.store - Privacy-first analytics backend that proxies appcast requests
- GitHub Releases - Hosts the actual DMG files and appcast XML files
System Architecture
Update Check Flow
- App initiates update check: VibeTunnel queries Stats.store endpoint
- Stats.store logs analytics: Records anonymous data (OS version, CPU type, daily unique users)
- Stats.store proxies request: Fetches appcast.xml from GitHub
- Appcast returned: Stats.store returns the appcast to the app
- Signature verification: Sparkle verifies the EdDSA signature
- Direct download: If valid, app downloads DMG directly from GitHub (not through Stats.store)
Update Endpoints
- Stable channel:
https://stats.store/api/v1/appcast/appcast.xml
- Pre-release channel:
https://stats.store/api/v1/appcast/appcast-prerelease.xml
Initial Setup and Registration
Prerequisites
Before Stats.store can serve your appcast files, you need to:- Register your application with Stats.store
- Configure your app to use Stats.store endpoints
- Ensure proper User-Agent headers are sent
Stats.store Registration
Important: As of the beta 9 release, VibeTunnel shows “Application not found” when querying Stats.store, indicating the app may not be properly registered or configured. To register your app with Stats.store:- Visit stats.store and create an account
- Add your application with:
- App name:
VibeTunnel
- Bundle ID:
sh.vibetunnel.vibetunnel
- GitHub repository:
amantus-ai/vibetunnel
- App name:
- Configure the appcast URLs to proxy to:
- Stable:
https://raw.githubusercontent.com/amantus-ai/vibetunnel/main/appcast.xml
- Pre-release:
https://raw.githubusercontent.com/amantus-ai/vibetunnel/main/appcast-prerelease.xml
- Stable:
Verifying Configuration
Check if your app is properly configured:Configuration Details
App Configuration (Info.plist)
HTTP Requirements
Stats.store requires proper app identification via User-Agent header:Key Management and Signatures
Public Key
- Location: Info.plist (
SUPublicEDKey
) - Value:
AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=
- Purpose: Verifies EdDSA signatures of updates
Private Key
- Location:
private/sparkle_private_key
- Purpose: Signs DMG files for appcast entries
- Critical: Must match the public key in Info.plist
Signature Generation
To generate a signature for a DMG file:sign_update
without the -f
flag as it may use a different key from the keychain.
Appcast XML Format
Structure
Critical Fields
- sparkle:version: Build number (must be incrementing)
- sparkle:shortVersionString: Human-readable version
- length: Exact file size in bytes
- sparkle:edSignature: EdDSA signature of the DMG file
- url: Direct download link to GitHub release
Testing and Verification
Manual Testing
Stats.store Caching
⚠️ Important: Stats.store has a 1-minute cache for appcast files. After updating the appcast on GitHub:- Wait at least 1 minute before testing
- The old version may be served during this cache period
- Force-refresh won’t bypass this server-side cache
Current Release Signatures
Complete signature reference for all VibeTunnel beta releases:Version | Build | File Size | Signature |
---|---|---|---|
1.0.0-beta.1 | 121 | 39,418,009 | lm3eCKxuykGYj1oRG3uRm3QB+3azo7EGGeuP2SzZHsobnKGBxq48H21rN9WDi2mry8NbGM9YwjdjfzS56h7GDA== |
1.0.0-beta.2 | 133 | 40,511,292 | VcPuSbUbcqhwrqongx9+mLhVAuHWlCw+xzIvsvqYKEv6W8UWtUPlPkYCgvoLuNRrJMnEOFcX/eJJv5RQl9/qAQ== |
1.0.0-beta.3 | 140 | 43,073,375 | kY87vo1HXpFx6aKb9LDXbe/AmQND5iH+W7a3qpf2AejmEl+i7wKch/JY3zhBHrmWIuksiKOwFIIklT4sQFMjDw== |
1.0.0-beta.4 | 151 | 43,169,474 | QXjzgcZXuF4zAy1AeYXAS2+WXLYWmMQYcm46isVO3WRp3I3IPHrXLOmWlVFixsFMM3JCKRmOnYsftEAyWjGbAA== |
1.0.0-beta.5 | 157 | 43,227,774 | wAhA+mtSpcXd4f62yyF4bzSt/IG9ynPPVIRmIwcMCBgCZh0mavixiEPUHxYMGlukVuC+TXLJfqXowiCwMH8tBQ== |
1.0.0-beta.6 | 159 | 43,312,816 | g84r8XLzvfeVHccjULfpjRGClf9Wll14PVLXCktUBkc+TRA312troC8dw1+bEn/ta5itW7nErwOCCIGD8U21DA== |
1.0.0-beta.7 | 165 | 43,383,612 | vdcImChUp1qKY3V/8CTnyxq0TXkQjPXnEbEvks0xwWbzqvSP1xe3MBr/5kalilFpC9dH7wMxO9ohoNhHTjOvBQ== |
1.0.0-beta.8 | 172 | 44,748,347 | /538z6L/qhhnHkfWU1hVoqeKvFdHubFRobfq6Vfmwz4UCpDVhJrqG+W28xW1wU4W9+xt41NMgei+DLJr1JV8Cg== |
1.0.0-beta.9 | 173 | 44,748,582 | xAzHFZ1FYtncpZx1xKMAIMT9kDkEiZfH1uuY80weKzi7JE8Yd673/7919f3D3g4j/B7fTMs88TVlTxocL9zRCw== |
Fallback Options Without Stats.store
If Stats.store is not configured or you need to release before registration is complete, you can use direct GitHub URLs:Temporary Direct Configuration
UpdateUpdateChannel.swift
to use GitHub directly:
Implications of Direct URLs
Pros:- Works immediately without registration
- No dependency on third-party service
- Updates still function normally
- No analytics or usage statistics
- No geographic CDN benefits
- No A/B testing capabilities
- Missing crash/update correlation data
Migration Path
- Release with direct URLs if Stats.store isn’t ready
- Register with Stats.store when convenient
- Update app in next release to use Stats.store endpoints
- Existing users will update and start using Stats.store
Benefits of Stats.store
-
Privacy-First Analytics:
- Track update adoption rates without collecting personal data
- Monitor OS version distribution and hardware stats
- Daily unique users via salted IP hashes (change daily)
- No IP tracking, device IDs, or fingerprinting
-
Anonymous Data Collection:
- macOS version and CPU architecture
- App version numbers
- Hardware info (RAM, Mac model, core count)
- System language (no location data)
-
Technical Benefits:
- Transparent proxy (doesn’t host files)
- 1-minute appcast caching
- GitHub outage protection
- Free for open source projects
-
Future Features (Planned):
- A/B testing for gradual rollouts
- Custom update channels
- Geographic CDN capabilities
Troubleshooting Guide
Common Issues
”Application not found” Error
This is the most common Stats.store integration issue. There are several potential causes:-
App not registered with Stats.store
- Solution: Register at stats.store
- Create account and add VibeTunnel as an application
- Configure GitHub repository URLs for appcast proxying
-
Incorrect User-Agent header
- Required format:
VibeTunnel/VERSION Sparkle/VERSION
- Example:
VibeTunnel/1.0.0-beta.9 Sparkle/2.7.1
- Fix: Ensure Sparkle framework is properly integrated
- Required format:
-
Bundle ID mismatch
- Expected:
sh.vibetunnel.vibetunnel
- Check: Verify in Info.plist that CFBundleIdentifier matches
- Expected:
-
Testing before registration
- Note: You can still release without Stats.store
- Alternative: Use direct GitHub URLs in Info.plist temporarily
- Update later: Once registered, update SUFeedURL to Stats.store endpoints
Signature Verification Failed
- Cause: Wrong private key used for signing
- Fix: Use
sign_update -f private/sparkle_private_key
Updates Not Detected
- Cause: Stats.store cache or incorrect version numbers
- Fix: Wait 1 minute after updating appcast, verify version increments
File Size Mismatch
- Cause: DMG was modified after signing
- Fix: Re-download and re-sign the DMG
Debugging Commands
The Beta 8 Update Incident (July 2025)
Timeline of Events
- Initial Success: Updates from beta 1 through beta 7 worked correctly
- Problem Detected: Users updating from beta 7 to beta 8 received error:
“The update is improperly signed and could not be validated”
- Investigation: Discovered multiple Sparkle private keys on the system
- Root Cause: Wrong private key used to generate appcast signatures
- Resolution: Updated appcast with correct signature
Technical Details
The Problem
Two different Sparkle private keys existed:-
File-based key (
private/sparkle_private_key
)- Base64:
SMYPxE98bJ5iLdHTLHTqGKZNFcZLgrT5Hyjh79h3TaU=
- Matches public key:
AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=
- This is the correct key
- Base64:
-
Keychain key (accessed without
-f
flag)- Different key stored in macOS keychain
- Produces incompatible signatures
- This was incorrectly used
The Investigation
Usedsign_update
to test both keys:
The Mystery
Why did beta 1-7 updates work despite incorrect signatures? Theories:- DMGs might have been originally signed with the keychain key
- The keychain key might have changed between releases
- There could have been a different issue masking the problem
The Solution
- Identified the correct private key file
- Generated the correct signature using
-f
flag - Updated only the appcast XML (no DMG changes needed)
- Waited for Stats.store cache to expire (1 minute)
- Verified updates now work correctly
Key Lessons Learned
- Always use
-f
flag:sign_update -f private/sparkle_private_key
- Document key locations: Keep clear records of which keys to use
- Understand the architecture: Signatures live in appcast, not DMG files
- Remember caching: Stats.store has a 1-minute cache
- Test thoroughly: Verify signatures match before releasing
Prevention Measures
- Created this comprehensive documentation
- Added warnings about multiple keys
- Established correct signing procedure
- Documented all historical signatures for reference