Photos Backup for iOS
An experimental, fully on-device iPhone app for backing up selected photos, videos, and albums to Google Photos. It is a single SwiftUI app that completes Google account setup in an in-app web view, so everything happens on the phone without a desktop companion or hosted service.
[!WARNING]
This project uses Google's private, undocumented Photos endpoints and an
Android-style authentication flow. It is not affiliated with or endorsed by
Google, and the integration may stop working without notice. Treat it as
experimental software and use it at your own risk.
What it can do
- Connect a Google account through Google's EmbeddedSetup flow, in an in-app web view.
- Capture the single-use
oauth_tokenin-process from the web view's cookie store. - Exchange the token for a Google Photos credential entirely on the device.
- Select albums from the local Photos library.
- Queue individual photos, videos, or all items in selected albums.
- Show hashing, duplicate-check, upload, and finalization progress per item.
- Avoid re-uploading media already present in Google Photos.
- Retry transient failures, cancel work, and resume after reconnecting.
- Show why an upload failed in Google's own words, copyable from the row and
- Restore pending album uploads after an app restart and remember completed
- Show per-album backup progress, and re-upload assets edited after backup.
- Back up an edited photo as it looks on the iPhone, the file the Google Photos
- With Back Up Live Photo Motion on, back up a Live Photo with its motion
- Back up a photo edited in the Google Photos app so that app counts it as
- Upload in original quality or request Google's Storage Saver processing.
- Choose how many uploads run at once, from 1 to 10.
- Enforce Wi-Fi-only or Wi-Fi-and-cellular policy at queue and request level,
- Keep backing up after you leave the app, on iOS 26 and later. While the app
- Request recurring iOS background-processing windows for selected-album backup.
- Expose a Back Up Photos Shortcuts action on iOS 16+ for charger,
- Keep file PUTs running in an iOS-owned background
URLSession, then commit
- Track PhotoKit persistent changes on iOS 16+ so backdated imports are found.
- Keep going when one item keeps closing the app: an item the app stopped on
- Create a privacy-safe diagnostic report with a plain-language summary, the
- Store usable long-lived credentials in the iOS Keychain when signing permits.
Current status
The complete authentication path has been proven on an iOS 17 device and
simulator: the in-app web view receives the oauth_token, the app reads it from
the web view's own cookie store, exchanges it for an unbound master token and
Photos credential, and an authenticated photosdata-pa request succeeds.
The Xcode project, app target, and scheme are named PhotosBackup; the
user-facing app is named Photos Backup.
Latest release: 0.3.6 (releases). 161 tests run on an iPhone simulator: 158 pass. The 2 opt-in live tests and the Keychain round trip, which needs a signed build, are skipped.
App identity (since 0.0.2)
| Piece | Value |
| --- | --- |
| App bundle ID | com.g8row.photosbackup |
| Background task | com.g8row.photosbackup.background-backup |
| Continued backup (iOS 26+) | , permitted by |
| Background upload session | com.g8row.photosbackup.background-upload |
[!IMPORTANT]
The bundle ID and Keychain service changed in 0.0.2. After updating from an
older build, reconnect the Google account once, then force-quit and reopen
to confirm it stays connected.
Reporting a problem
Open Settings → Support → Create Diagnostic Report, tap **Generate Report, then Share or Save Report**. Attach the text file to a GitHub issue. The report opens with a short What stands out list that often explains the problem on its own. It leaves out credentials, account addresses, photo identifiers, filenames, media, and request URLs.
Diagnostics → Event Timeline shows the same timeline inside the app. Crash details from iOS are included only when Share With App Developers is on (Settings → Privacy & Security → Analytics & Improvements); they arrive a day or so after the crash.
Requirements
- macOS with Xcode 16.4 and an installed iOS Simulator runtime
- XcodeGen 2.40 or newer
- iOS 15.0 or newer
- A Google account for the live connection flow
- For a physical device: an Apple signing identity, or a sideloading tool such
Install XcodeGen with Homebrew if needed:
brew install xcodegen
Build and run
Generate the Xcode project:
xcodegen generate
open PhotosBackup.xcodeproj
Select the PhotosBackup scheme and an iPhone simulator in Xcode, then run the
app. A command-line simulator build also works:
xcodebuild \
-project PhotosBackup.xcodeproj \
-scheme PhotosBackup \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
build
For a signed device build, set DEVELOPMENT_TEAM in project.yml, regenerate
the project, and let Xcode manage signing.
Test background execution
Debug builds expose Settings → Diagnostics → Simulate Background Run. This runs the same scan/enqueue/wait path immediately and is the fastest normal test loop.
To exercise the actual BGProcessingTask launch handler on a connected device,
run the app from Xcode, background it, pause the debugger, and enter this in the
LLDB console:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.g8row.photosbackup.background-backup"]
The Diagnostics screen also has a button that copies this command.
Build an unsigned IPA
The repository includes a packaging script for SideStore/AltStore-style sideloading:
./Scripts/make-ipa.sh
The script defaults to DEVELOPER_DIR=/Applications/Xcode-16.4.0.app/Contents/Developer.
Override it only if Xcode lives elsewhere:
DEVELOPER_DIR=/path/to/Xcode.app/Contents/Developer ./Scripts/make-ipa.sh
The unsigned package is written to build/PhotosBackup.ipa. The sideloading
tool re-signs it with the Apple ID configured on the device.
Install via SideStore
Prebuilt unsigned IPAs are attached to each GitHub release.
To get new versions automatically, add this source in SideStore, AltStore, or Feather. It is regenerated from every release:
https://g8row.github.io/PhotosBackup/apps.json
[!TIP]
On your iPhone (with SideStore or AltStore installed), one-tap install of
the latest release:
> - Install Photos Backup —
open on the iPhone and tap Install with SideStore / AltStore.
> GitHub strips customsidestore:///altstore://URL schemes in markdown,
so the buttons live on that page instead of directly in this README. It
installs
https://github.com/g8row/PhotosBackup/releases/latest/download/PhotosBackup.ipa.
- AirDrop
PhotosBackup.ipato the iPhone and save it in Files. - Turn on LocalDevVPN.
- In SideStore, tap +, choose
PhotosBackup.ipa, and install it. - After updating across the 0.0.2 bundle-ID change, reconnect the Google
Connect a Google account
- Install and launch Photos Backup.
- In onboarding (or Settings → Connect Account), tap Connect Google Account.
- Sign in and accept Google's consent prompt in the in-app window. The page may
- Grant the desired Photos access and select albums.
oauth_token is single-use and is read once from the web view's
cookie store, then the web session is discarded.
Authentication and credential handling
The normal flow is:
In-app EmbeddedSetup web view
│ oauth_token (read from WKHTTPCookieStore)
▼
Android master token → Photos access token → private Photos API
- The exchange runs locally; there is no companion backend.
- Credentials are stored as a single Keychain item using
AfterFirstUnlockThisDeviceOnly when Keychain access is available.
- Exported Photos-library items are staged in protected Application Support,
- The
oauth_tokenis read in-process from the app's own non-persistent web
- Bound/encrypted Google tokens are rejected because token binding is not
Known limitations
- Google can change or disable the private authentication and Photos endpoints.
- The Google Photos app's Free up space skips edited photos (including Live
- A photo or video saved to the iPhone from Google Photos (a file with a name
AIXW8346.JPG) never counts as backed up in the Google Photos app, even
though the library already holds it: an upload of the same bytes merges into
that existing item. Any edit in Apple Photos makes a new file, which then
counts once backed up.
- An edited photo or video is backed up as it looks on the iPhone. Its
- Background album backup is opportunistic: iOS decides when each processing
- Continuing after you leave the app needs iOS 26, and iOS accepts the request
- Shortcuts can create extra backup opportunities on iOS 16+, but iOS gives
- Background scans enqueue bounded batches of 250. The limit bounds memory, not
- Export, hashing, duplicate lookup, and upload initialization still need an
- Cloud-only PhotoKit resources are deferred during short background processing
- Unsigned simulator builds cannot persist the credential in the Keychain.
- Google accounts that receive a bound/encrypted master token are unsupported.
- This is not an App Store-ready release.
Tests
Run the offline unit test suite against any installed simulator:
xcodebuild \
-project PhotosBackup.xcodeproj \
-scheme PhotosBackup \
-destination 'platform=iOS Simulator,name=<your simulator>' \
CODE_SIGNING_ALLOWED=NO \
test
List available simulator names with:
xcrun simctl list devices available
Live tests are opt-in because they contact Google. The full exchange test also
requires a fresh, single-use oauth_token:
TEST_RUNNER_GPMC_LIVE=1 \
xcodebuild ... test \
-only-testing:PhotosBackupTests/LiveExchangeTests/testInvalidTokenIsRejectedByGoogleNotByUs
TEST_RUNNER_GPMC_LIVE=1 \
TEST_RUNNER_GPMC_OAUTH_TOKEN=oauth_XXXX \
xcodebuild ... test \
-only-testing:PhotosBackupTests/LiveExchangeTests/testFullExchangeWithRealToken
Never commit tokens or captured account credentials.
Repository layout
App/Sources/ SwiftUI app, onboarding, account, and upload queue
App/Sources/AutomaticBackupCoordinator.swift BGProcessingTask scheduling
App/Sources/ContinuedBackup.swift iOS 26 continued backup after leaving the app
App/Sources/BackgroundUploadTransport.swift Relaunch-safe file PUT transport
App/Sources/PhotoLibraryChangeTracker.swift Persistent PhotoKit scan token
App/Sources/NetworkPolicy.swift Wi-Fi-only / cellular enforcement
App/Sources/UploadQueuePersistence.swift Durable account-scoped queue
App/Resources/ Info.plist and app icon assets
App/Sources/AccountConnectWebView.swift In-app EmbeddedSetup web view
GPMC/Core/ Photos protocol client and protobuf helpers
Tests/PhotosBackupTests/ Offline unit tests and gated live tests
Scripts/make-ipa.sh Unsigned IPA packaging
docs/ Feasibility log and authentication ADR
project.yml XcodeGen project definition
For implementation history and protocol details, see:
Acknowledgements
The protocol work is based on GPMC by xob0t, and the browser authentication route is based on gotohp. The pinned upstream revisions and design rationale are recorded in the authentication ADR.
License
This project is available under the MIT License.