You sit down with a fresh clone and a first ticket that sounds harmless on Slack. The ticket asks you to persist the last voice session so offline replay still works after the process dies. Nobody mentions backup, restore, or what happens when a reviewer asks you to roll the PR back. The trap is not the schema; it is whether that file rides into iCloud or Google backup after you merge.
This is a proposed first-hour workflow for a junior engineer joining a mobile AI repo, not a scored lab report. You will treat backup exclusion as a merge gate, then rehearse restore after rollback. Record the device, OS, framework, permission state, and network condition before you claim anything recovered, restarted, or silently disappeared.
On-device voice and offline replay almost always grow a local store during the first PR. That store holds utterances, partial captions, and sometimes speaker labels that never belonged in a cloud sync channel. Phone backup is an OS mechanism, not your product sync, and it can copy those files while the app is backgrounded or even uninstalled later. If your first cache lands in Documents, shared storage, or a default database path, a restore can resurrect user speech after you thought the rollback was clean.
You should assume AI-drafted persistence will pick the convenient directory unless you constrain it. Convenient directories are the ones backup systems already know how to upload. Your job in hour one is not to debate schema elegance. Your job is to prove the transcript file is excluded, then prove a restore cannot bring it back after revert.
Fill this block in the PR description before you paste commands. Do not invent numbers; leave blanks until a device actually answers you.
If you cannot name the OS backup transport, you are not ready to merge the cache. Write that limitation in the PR instead of calling the feature offline-ready.
Label this as an unexecuted checklist until you run it on one physical device. Do not copy a lifecycle matrix from another PR and call it coverage.
You are looking for three outcomes only: recovered, restarted empty, or silently disappeared. Anything else belongs in limitations, not in a pass comment.
Keep the SQLite file in Application Support, not in Documents, then set the exclusion flag on the file URL. Documents is user data from the system's point of view, which makes it a backup candidate even when you consider it a cache. Caches can be purged under storage pressure, so it is the wrong place for replay that must survive a cold start. Application Support plus an explicit exclusion is the usual honest compromise.
import Foundation
enum TranscriptStore {
static func url() throws -> URL {
let root = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let dir = root.appendingPathComponent("voice-replay", isDirectory: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("transcripts.sqlite")
}
static func excludeFromBackup() throws {
var url = try url()
var values = URLResourceValues()
values.isExcludedFromBackup = true
try url.setResourceValues(values)
}
}
Call excludeFromBackup() after the first successful write, then read the flag back. A write that never sets the resource value will look fine in the simulator until someone restores a laptop backup. Proposed check: if isExcludedFromBackup is not true, fail the debug launch, not just a log line.
func assertExcludedFromBackup(_ url: URL) throws {
let values = try url.resourceValues(forKeys: [.isExcludedFromBackupKey])
precondition(values.isExcludedFromBackup == true, "transcript store is backup-eligible")
}
Android Auto Backup and device-to-device migration read your manifest, not your code comments. A default android:allowBackup="true" with no exclusion rules will pack app databases when Google backup runs. Android 12 and later also honor dataExtractionRules for cloud backup versus device transfer, so you should ship both the older and newer XML. Put transcripts in internal storage, never in shared storage, and name the file so a reviewer can grep it.
<!-- AndroidManifest.xml -->
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules">
php
<!-- res/xml/backup_rules.xml -->
<full-backup-content>
<exclude domain="database" path="transcripts.db" />
<exclude domain="file" path="voice-replay/" />
</full-backup-content>
php
<!-- res/xml/data_extraction_rules.xml -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="database" path="transcripts.db" />
<exclude domain="file" path="voice-replay/" />
</cloud-backup>
<device-transfer>
<exclude domain="database" path="transcripts.db" />
<exclude domain="file" path="voice-replay/" />
</device-transfer>
</data-extraction-rules>
If the product must restore settings across phones, exclude only the utterance store, not the entire app. A blanket allowBackup="false" hides the problem from review and breaks legitimate preference migration. Call that tradeoff out in the PR so QA does not treat a missing backup as a privacy win.
Flutter's getApplicationDocumentsDirectory() maps to backup-eligible locations on both platforms more often than juniors expect. Prefer application-support or internal files, then still set the native exclusion because Dart cannot see iCloud flags by itself. React Native file helpers have the same split: a path that works offline is not automatically excluded from Auto Backup. Add a tiny native module or config plugin that sets the iOS resource value and ships the Android XML, then fail CI if those files are missing from the merged artifacts.
// Proposed Flutter check, not a measured benchmark.
final support = await getApplicationSupportDirectory();
final file = File('${support.path}/voice-replay/transcripts.sqlite');
await file.parent.create(recursive: true);
await file.writeAsBytes(bytes, flush: true);
// Call a MethodChannel that sets isExcludedFromBackup / verifies XML merge.
Do not let the first PR store transcripts beside model shards or logs. Mixed directories make exclusion rules brittle, and the next engineer will add a screenshot cache that inherits the wrong policy.
These commands inspect state. They are not exploits, and they will not print a pass unless you read the output against the expected path.
iOS Simulator, proposed:
xcrun simctl list devices booted
APP_DATA=$(xcrun simctl get_app_container booted com.example.voiceapp data)
find "$APP_DATA" -name 'transcripts.sqlite'
xattr -p com.apple.MobileBackup "$APP_DATA/Library/Application Support/voice-replay/transcripts.sqlite"
A value of 1 on com.apple.MobileBackup means excluded. Missing xattr after a write means your Swift flag never stuck. Re-run after a simulated restore if your Xcode version exposes that flow; otherwise use a device and say so.
Android debug build, proposed:
adb shell getprop ro.build.version.release
adb shell dumpsys package com.example.voiceapp | grep -A4 -i backup
adb shell run-as com.example.voiceapp ls -la files/voice-replay databases
adb shell bmgr backupnow com.example.voiceapp
If run-as fails, you are not on a debug install and you should stop. Do not switch to world-readable storage just to make listing easier. Paste the command output into the PR so reviewers can see the path and the backup flags together.
Your first rollback rehearsal should restore a backup taken while the bad cache still existed. Uninstalling the debug build only proves the package is gone, not that the OS forgot the utterances. A teammate who later restores the phone for a device swap can resurrect speech that your revert never deleted from backup storage.
Walk this sequence on one device and write the outcome in one sentence.
transcripts.sqlite or transcripts.db during an offline session.
If the restored app shows the old utterance, the first PR is not rolled back in any user-visible sense. Fix exclusion, wipe the local file on version downgrade, and repeat the restore. Do not call that wipe a privacy feature unless you also proved backup no longer contains the bytes.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. A junior can use MonkeyCode's free model access and free server option to draft the exclusion helper, the XML stubs, or a PR checklist from this article. That is a reasonable way to get a first patch onto the branch without waiting on a paid seat. It does not replace the device commands, and it does not prove backup exclusion by itself.
Ask the model for the resource-value call and the two Android XML files, then you run the inspection on hardware. If the draft points at Documents, shared storage, or allowBackup="false" with no explanation, reject it the same way you would reject a hand-written patch. Keep the chat log out of the PR; attach the xattr, dumpsys, and bmgr output instead.
| First-PR choice | Offline replay after kill | Likely backup behavior | Rollback after restore |
|---|---|---|---|
| Documents or default app database, no exclusion | Works | Transcripts often included | Old utterances can return |
| Caches directory only | May vanish under storage pressure | Sometimes skipped, not guaranteed | Replay randomly dies |
| Application Support or internal files, exclusion set, XML merged | Works if you re-open the store | Expected exclude if flags stick | Restore should not resurrect speech |
allowBackup="false" for the whole app |
Works | Settings and honest data also skipped | Hides the review signal |
Use the third row unless product, legal, or accessibility requires cross-device transcript restore. If you do require restore, encrypt at rest, document the transport, and stop calling the feature on-device-only.
This workflow does not measure backup bytes, radio wake, or battery drain, and you should not invent those figures. Exclusion flags can lag across OS versions, work profiles, and manufacturer backup apps that ignore Google’s XML. Simulator xattr checks are weaker than a device restore, and bmgr output varies by Play services and the selected transport. Cross-platform plugins may generate a second database file whose name never appears in your rules.
Do not follow this exclusion path if your product must legally retain conversation history across device replacement. Do not use it as a substitute for server-side deletion, because a file that never entered backup can still live in crash dumps or screenshots. Security reviewers, not the on-call junior, should decide whether device-to-device transfer is allowed for any remaining metadata.
If you run this on one phone, post the device, OS, the exact transition, and whether the transcript recovered, restarted empty, or silently disappeared. Comparable environment evidence is the only thing that makes the next junior’s first PR safer than yours.