Skip to content

Repository files navigation

Snapshotter

A native macOS menu bar backup manager powered by restic.

Snapshotter supports local folders, S3-compatible object storage, SFTP, and rest-server destinations. Repository encryption passwords and remote service credentials can be stored in macOS Keychain or a synced 1Password vault. SFTP uses the system SSH agent, configuration, and keys.

Project layout

  • app/ — React and TypeScript interface
  • macos/ — native Swift menu bar host and system integration
  • engine/ — Go backup-domain service and restic integration boundary

Development

Install dependencies once:

zsh -ic 'pnpm install'

Then run the app from the repository root using two terminals.

Terminal 1 starts the React development server:

zsh -ic 'pnpm dev'

Wait until Vite prints http://localhost:4173/. Terminal 2 starts the native menu-bar host:

make engine
swift run --package-path macos

A shield icon appears in the macOS menu bar. Click it to open the app. No Dock icon or regular window appears because Snapshotter runs as a menu-bar accessory. Stop the native process with Control-C. In a packaged build, right-click the menu-bar icon and choose Quit Snapshotter.

Debug builds enable WKWebView inspection. Right-click inside the popover and choose Inspect Element, or attach through Safari's Develop menu.

If the shield is not visible, check the menu-bar overflow area and confirm the Vite terminal is still running. Build each layer independently with:

zsh -ic 'pnpm build'
make engine
swift build --package-path macos
cd engine && go test -race ./...

Before publishing a change, run the complete release gate:

make verify

This checks Biome, UI tests, the production frontend, Go race tests, strict Swift compilation, app signing, the bundle plist, and DMG integrity.

Remote backends have an opt-in live lifecycle test. Supply a unique test repository location and credentials through SNAPSHOTTER_TEST_REPOSITORY_* environment variables, then run:

cd engine
go test ./resticadapter -run '^TestLiveRemoteRepository$' -count=1

The test creates an encrypted repository, backs up and restores a file, checks the repository, then deletes the snapshot and prunes its data. Never target a production repository.

The host loads the Vite server in debug builds. Release packaging will bundle the generated web assets into the application.

Packaging

Create a signed application bundle at build/Snapshotter.app:

make app
open build/Snapshotter.app

Packaging automatically uses the first valid identity in the login Keychain. A specific identity can be selected for a distributable build:

CODESIGN_IDENTITY="Developer ID Application: Example (TEAMID)" make app

Use CODESIGN_IDENTITY=- make app only for a throwaway build. Ad-hoc signatures change when the application changes, so macOS cannot persist Keychain access for them across builds.

Create a compressed installer image at build/Snapshotter.dmg:

make dmg

For release distribution, configure an Apple notary profile and sign with your Developer ID. The disk image is submitted, stapled, and verified automatically:

CODESIGN_IDENTITY="Developer ID Application: Example (TEAMID)" \
NOTARY_PROFILE="snapshotter-notary" make dmg

Releases (GitHub Actions)

The .github/workflows/release.yml workflow builds a signed and notarized .app (zipped) and .dmg on a macos-14 runner and publishes a GitHub Release.

Two channels are supported, selected automatically from the tag:

  • v1.2.3 — stable release.
  • v1.2.3-rc.1, v1.2.3-beta.1, ... (any tag with a hyphen pre-release part) — pre-release.

You can also trigger the workflow manually (Actions → release → Run workflow) to build a pre-release from any ref, optionally pinning a tag via the tag input.

Full signing is required and only works when these repository secrets are configured:

  • MACOS_CERTIFICATE — base64 of a Developer ID Application certificate .p12.
  • MACOS_CERTIFICATE_PWD — the .p12 password.
  • MACOS_CERTIFICATE_ID — the identity name, e.g. Developer ID Application: Snapshotter (TEAMID).
  • APPLE_ID — the Apple ID email used for notarization.
  • APPLE_TEAM_ID — the 10-character team ID (also shown on the Developer ID certificate), e.g. CWYUH669FD.
  • APPLE_APP_PASSWORD — an App Store account app-specific password created at appleid.apple.com → Sign-In & Security → App-Specific Passwords. No App Store Connect API key or access request is needed.

Releases also embed a 16:9 changelog-aware banner rendered in CI (Snapshotter-banner.png). Without AI it uses a static on-brand template. To have the banner designed automatically from the changelog, additionally set:

  • OPENCODE_API_KEY — key for an OpenAI-compatible model endpoint.
  • OPENCODE_BASE_URL — the endpoint base URL (e.g. an OpenAI-compatible API).
  • OPENCODE_MODEL_ID — the model ID to send.

When all three are set, the workflow installs opencode2 and runs it with the design-promotional-banner skill against the gathered changelog; if that fails, the pipeline falls back to the static template so a release never ships without a banner.

Architecture

The app never shells out to the restic executable. The Go engine is linked into the native application and exposes a small asynchronous API. Credentials are never persisted in UI state. Keychain storage is handled by the native layer; optional 1Password storage uses the official Go SDK and the 1Password desktop app.

To use 1Password storage, install and sign in to 1Password for Mac, then enable Settings > Developer > Integrate with 1Password SDKs. Enable Touch ID under Settings > Security to authorize Snapshotter with biometrics. During repository setup, select 1Password, choose a detected account, authorize access, and choose a vault. On macOS, Snapshotter detects active accounts from 1Password's read-only local metadata and presents them by display name; manual entry remains available if detection is unavailable. Snapshotter stores only the account UUID, vault ID, and item ID in its preferences. The repository password and remote-backend credentials remain in the synced 1Password item. On another Mac, choose the same destination, load the same vault, and select the synced Snapshotter item instead of entering the secrets again. Snapshotter stores the repository type and destination in that item and adds recovery instructions to its notes, so the item can be used to reconnect after reinstalling macOS. Disconnecting a repository leaves its synced item in 1Password so other devices do not lose access; remove that item manually when it is no longer needed anywhere.

The official SDK loads 1Password's signed IPC client library from the desktop app. Because that library is signed by 1Password rather than Snapshotter, packaged builds use the hardened-runtime library-validation exception required for this integration.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages