A native macOS reader for Beehiiv newsletters. Hardcoded to Cellular Thinking for v1, with an editorial reading experience (warm linen, serif body, drop caps, progress bar), archive browsing, auto-updates via Sparkle, and signed + notarized distribution.
- Download: latest DMG
- Landing page: https://app.cellularthinking.com
Requires Xcode 26+ and macOS 14+.
swift build
swift test
swift run BeereaderFor a signed/notarized local build (requires DEVELOPER_ID, TEAM_ID, NOTARY_APPLE_ID, NOTARY_PASSWORD in the environment):
./package.sh
# → dist/Beereader-<VERSION>.dmgWithout those vars the script still produces an unsigned .app and .dmg for local smoke-testing.
Releases are driven entirely by the VERSION file.
To ship a new version:
- Bump
VERSION(e.g.0.2.5→0.2.6). - Update
CFBundleShortVersionStringandCFBundleVersioninResources/Info.plistto match. - Commit, open a PR, merge to
main.
That's it — no git tag, no manual release. When the merge lands on main, CI:
- Builds + tests (
build-and-testjob). - If
VERSIONdoesn't yet have a matchingv$VERSIONtag, theauto-tagjob creates and pushes it. - The
packagejob checks out that tag and runs the full pipeline:package.sh→ signed.app→ notarized → stapled → DMG → GitHub Release → Sparkle-signed →docs/appcast.xmlpushed to GitHub Pages.
Sparkle clients installed in the wild pick up the new build from the appcast automatically.
If VERSION wasn't bumped, auto-tag no-ops and the package job is skipped — the merge is a normal code-only change.
If you ever need to cut a release outside the main-push flow (e.g. hotfix from a branch), you can still git tag v0.2.x && git push --tags directly. The existing tag-push trigger takes that path through the same package job and skips auto-tag.
| Secret | Purpose |
|---|---|
CERTIFICATE_P12 |
base64-encoded Developer ID Application .p12 |
CERTIFICATE_PASSWORD |
password for the .p12 above |
APPLE_TEAM_ID |
10-char team ID (e.g. ABC123DEFG) |
APPLE_ID |
Apple ID email used for notarization |
APPLE_ID_PASSWORD |
app-specific password (NOT your Apple ID password) |
SPARKLE_PRIVATE_KEY |
ed25519 private key used to sign the DMG in the appcast |
No extra PAT is required — auto-tag uses the default GITHUB_TOKEN.
Sources/Beereader/
├── App/ BeereaderApp.swift — @main entry, window config
├── Models/ Article.swift, Publication.swift
├── Services/ FeedService, ArticleCache, ImageCache, HTMLSanitizer,
│ ArticleRenderer, ReadStateService, ReadingTime,
│ UpdateService (Sparkle), NotificationService
├── Utilities/ ResourceLoader — bypasses Bundle.module in packaged .app
└── Views/ ContentView, WebViewCoordinator, ArchiveSidebar,
WelcomeView, StateViews, HexagonMark
Resources/ Info.plist, Beereader.icns, directory.json
Sources/Beereader/Resources/
reader.css, Source Serif 4 + IBM Plex Sans WOFF2s,
author-sprite.png
docs/ index.html (landing page), appcast.xml (Sparkle feed),
screenshot.png
MIT. See LICENSE.