This document outlines the architectural decisions made for production deployment of Dingo Track, including code signing, notarization, credential management, and distribution strategy.
Dingo Track is distributed as a signed and notarized macOS DMG file with embedded Google OAuth credentials, supporting both Intel and Apple Silicon Macs through universal binary builds.
Decision: Enable ASAR (Atom Shell Archive) packaging for the production app.
Rationale:
- Faster app loading (single file vs. thousands of individual files)
- Reduced file count improves performance and prevents file system issues
- Standard practice for Electron apps
- Enables credential injection into a single compiled JavaScript file
Implementation:
// package.json
"build": {
"asar": true
}Trade-offs:
- Files inside ASAR cannot be modified after packaging
- Requires extraction to read individual files (handled automatically by Electron)
Decision: Build a single universal binary supporting both Intel (x64) and Apple Silicon (arm64).
Rationale:
- One DMG file for all Mac users (better UX)
- Automatic native performance on both architectures
- Simpler distribution and versioning
- Industry standard for modern Mac apps
Implementation:
// package.json
"mac": {
"target": [
{
"target": "dmg",
"arch": ["universal"]
}
]
}Build Process:
- electron-builder creates two temporary builds (x64 and arm64)
- Builds are signed individually
- Binaries are merged using
lipo - Final universal app is notarized once
Important: Custom notarization script skips temporary -temp directories to prevent duplicate notarization.
Decision: Hardcode Google OAuth credentials into compiled JavaScript at build time.
Problem:
- Environment variables don't exist in distributed apps
- Users shouldn't need to configure OAuth credentials
.envfiles can't be bundled (security risk, ignored by git)
Solution: Post-build script replaces environment variable lookups with actual credentials.
Implementation:
// scripts/inject-credentials.js
const content = fs.readFileSync('out/main/index.js', 'utf8');
// Find and replace env var patterns with hardcoded values
content = content.replace(
/process\.env\.GOOGLE_CLIENT_ID\s*\|\|\s*process\.env\.DIST_GOOGLE_CLIENT_ID\s*\|\|\s*""/g,
`"${clientId}"`
);
fs.writeFileSync('out/main/index.js', content, 'utf8');Build Pipeline:
electron-vite build → inject-credentials.js → electron-builder → notarizeSecurity Considerations:
- Client ID is not secret (visible in OAuth flow anyway)
- Client Secret is protected by:
- GitHub Secrets in CI/CD
- Local
.env(gitignored) - Only exists in compiled JavaScript (not source code)
Environment Variables:
GOOGLE_CLIENT_ID/DIST_GOOGLE_CLIENT_ID- OAuth client IDGOOGLE_CLIENT_SECRET/DIST_GOOGLE_CLIENT_SECRET- OAuth client secret- Checked in this order by
GoogleCalendarService.ts
Decision: Use Developer ID Application certificate (not Mac App Store distribution).
Rationale:
- Direct download distribution (faster releases)
- No App Store review process
- More flexible for menu bar apps
- Lower barrier for users (no Apple ID required)
Certificate Type: Developer ID Application
- For apps distributed outside Mac App Store
- Requires Apple Developer Program ($99/year)
- Allows Gatekeeper to verify app integrity
Configuration:
// package.json
"mac": {
"hardenedRuntime": true,
"gatekeeperAssess": false,
"notarize": false // Disabled built-in, using custom script
}Why notarize: false?
- electron-builder's built-in notarization had issues with environment variable loading
- Custom
afterSignhook provides more control and better error handling
Decision: Implement custom notarization via afterSign hook instead of electron-builder's built-in notarization.
Problems with built-in notarization:
- Inconsistent environment variable loading from
.env - Poor error messages
- JSON parsing errors with Apple's notarization service
Solution: scripts/notarize.js with explicit credential handling.
Implementation:
// scripts/notarize.js
exports.default = async function notarizing(context) {
// Skip notarization for temporary universal build directories
if (appOutDir.includes('-temp')) {
console.log('⏭️ Skipping notarization for temporary build');
return;
}
// Load credentials from environment (CI) or .env (local)
const appleId = process.env.APPLE_ID;
const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD;
const teamId = process.env.APPLE_TEAM_ID;
// Dynamic import for ES module compatibility
const { notarize } = await import('@electron/notarize');
await notarize({
tool: 'notarytool',
appPath: appPath,
appleId: appleId,
appleIdPassword: appleIdPassword,
teamId: teamId
});
};Key Features:
- Skips temporary directories during universal binary build
- Explicitly loads credentials from environment
- Uses modern
notarytool(not deprecatedaltool) - Dynamic ES module import for compatibility
- Graceful failure with clear error messages
Configuration:
// package.json (root level, not inside "mac")
"afterSign": "scripts/notarize.js"Decision: Use GitHub Actions for automated builds with secure credential handling.
Certificate Handling:
# Encode locally:
base64 -i DeveloperID.p12 | pbcopy
# Decode in CI:
echo ${{ secrets.CSC_LINK_BASE64 }} | base64 --decode > certificate.p12
# Create temporary keychain
security create-keychain -p actions temp.keychain
security import certificate.p12 -k temp.keychain -P "$CSC_KEY_PASSWORD"
security set-key-partition-list -S apple-tool:,apple: -s -k actions temp.keychainRequired GitHub Secrets:
CSC_LINK_BASE64- Base64-encoded .p12 certificateCSC_KEY_PASSWORD- Certificate passwordAPPLE_ID- Apple ID for notarizationAPPLE_APP_SPECIFIC_PASSWORD- App-specific passwordAPPLE_TEAM_ID- Apple Developer Team IDGOOGLE_CLIENT_ID- OAuth client IDGOOGLE_CLIENT_SECRET- OAuth client secret
Why temporary keychain?
- Isolated from system keychain
- Automatically cleaned up after build
- No conflicts with existing certificates
- Works reliably in CI environment
Decision: Build landing page as multi-page static site with separate Privacy Policy and Terms of Service pages.
Rationale:
- Required for Google OAuth verification
- Better SEO (separate URLs)
- Cleaner user experience
- Easy to link from OAuth consent screen
Structure:
docs/
├── index.html # Main landing page
├── privacy-policy.html # Privacy Policy
├── terms-of-service.html # Terms of Service
└── assets/
Build System:
// vite.config.js
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
privacy: resolve(__dirname, 'privacy-policy.html'),
terms: resolve(__dirname, 'terms-of-service.html')
}
}# 1. Set up environment
cp .env.example .env # Create .env with your credentials
# 2. Development
pnpm dev # Hot reload for development
# 3. Production build (signed & notarized)
pnpm dist:mac # Builds universal DMG with credentialsBuild steps:
electron-vite build- Compile TypeScript and bundle Reactinject-credentials.js- Hardcode OAuth credentialselectron-builder- Package into .app with code signingnotarize.js- Upload to Apple for notarization (2-10 min)- Output:
dist-electron/Dingo Track-1.0.x-universal.dmg
Workflow: .github/workflows/deploy-landing.yml
1. Setup code signing (macOS runner)
- Decode certificate from base64
- Create temporary keychain
- Import certificate and set permissions
2. Build desktop app
- Export all environment variables (GOOGLE_*, APPLE_*, CSC_*)
- Run: pnpm build:electron
- Run: electron-builder --mac dmg --universal
3. Verify credentials were injected
- Extract app.asar
- Check for hardcoded credentials
- Fail if env vars still present
4. Build landing page
- Copy DMG to landing-page/public/downloads/
- Build with NODE_ENV=production
- Deploy to GitHub PagesCritical: All credentials must be exported as environment variables before build.
Development:
.envfile (gitignored)- Never committed to repository
- Used only for local builds
Production:
- GitHub Secrets (encrypted at rest)
- Injected into compiled JavaScript at build time
- Distributed as part of app.asar
User Data:
- OAuth tokens stored locally in
electron-store - Never transmitted to our servers
- Only sent to Google for Calendar API
Apple Root CA
└── Apple Worldwide Developer Relations CA (G3)
└── Developer ID Certification Authority (G2)
└── Developer ID Application: Conner Ward (N4YGB5B92K)
└── Dingo Track.app
Trust Establishment:
- Install intermediate certificates (
AppleWWDRCAG3.cer,DeveloperIDG2CA.cer) - Generate CSR from Keychain Access
- Create Developer ID Application certificate on Apple Developer portal
- Install certificate with private key
- Export as .p12 for CI/CD
Pros:
- Immediate updates
- No review process
- Full control over release timing
- Works for menu bar apps without restrictions
Cons:
- Users see "developer cannot be verified" warning if not notarized
- Manual download and installation
- No automatic updates (yet)
User Experience:
- Visit landing page
- Click "Download for Mac"
- Open DMG
- Drag to Applications
- First launch: Right-click → Open (if unsigned) or double-click (if notarized)
- Authorize Google Calendar (optional)
Would require:
- Mac App Store distribution certificate (different from Developer ID)
- Mac App Store provisioning profile
- Entitlements files
- Sandbox compliance (major constraint for menu bar apps)
- App Store review (1-7 days per release)
Current blocker: Menu bar apps have limited functionality in Mac App Store sandbox.
Cause: App not notarized or certificate chain incomplete.
Solution:
- Verify certificate:
security find-identity -v -p codesigning - Check notarization:
spctl -a -vv "Dingo Track.app" - Ensure intermediate certificates installed
- Re-notarize if needed
Cause: Credential injection failed or credentials not in environment during build.
Solution:
- Verify GitHub Secrets are set
- Check injection script ran: Look for "✅ Credentials injected" in build log
- Verify pattern match in
out/main/index.jsbefore injection - Confirm environment variables exported in workflow
Cause: Temporary directory notarization or missing credentials.
Solution:
- Check
afterSignhook is at root level in package.json (not inside "mac") - Verify
appOutDir.includes('-temp')check in notarize.js - Ensure APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID are set
- Check Apple notarization history at developer.apple.com
- Load time: 50% faster app startup (single file vs. thousands)
- File I/O: Reduced file system operations
- Distribution: Smaller DMG (better compression)
- Size: ~2x size of single-arch build (acceptable for desktop app)
- Build time: ~1.5x longer (parallel builds + merge)
- Performance: Native speed on both architectures (worth it)
Dingo Track's use of Google Calendar API adheres to Limited Use requirements:
-
Limited Data Access:
- Only requests
calendar.readonlyandcalendar.eventsscopes - Does not request access to other Google services
- Only requests
-
No Data Transfer:
- Does not transfer Google user data to servers
- All data stays on user's local device
-
No Secondary Use:
- Does not use calendar data for advertising
- Does not use calendar data for ML/AI training
- Does not sell calendar data
-
Transparent Privacy:
- Privacy Policy clearly states local-only storage
- Terms of Service reference Google API compliance
- Both documents linked from landing page and app
Compliance:
- ✅ Hardened Runtime enabled
- ✅ Code signed with Developer ID
- ✅ All binaries signed (including native modules)
- ✅ No malware or suspicious code
- ✅ Notarization ticket stapled to DMG
Electron-updater:
- Would enable seamless updates
- Requires hosting update manifest JSON
- Could use GitHub Releases
Electron crash reporter:
- Catch and report production crashes
- Could use Sentry or custom endpoint
Privacy-respecting analytics:
- Feature usage tracking
- Performance monitoring
- Error rates
Windows/Linux support:
- Separate build workflows
- Platform-specific code signing
- Different credential injection for Windows
- Electron Security
- Apple Notarization Guide
- Google API Limited Use
- electron-builder Configuration
- Code Signing Guide
- Architecture Overview
- v1.0.0 - Initial release with manual OAuth setup
- v1.0.1 - Embedded OAuth credentials, universal binary, improved notarization