Conversation
Requirement R0 says a synchronised device must stay fully usable for an unlimited time, even if the server is switched off for good. Two things stood in the way: secrets were never stored on the device, and an unreachable server blocked unlocking. Local secret store: - new Secret entity holding the OpenPGP ciphertext exactly as the server sent it, keyed by resource and cascade-deleted with it; database v24 with a purely additive, network independent migration - SecretInteractor now reads the local copy first and only asks the network for a secret this device has never stored, caching whatever it gets back - the resources index is requested with contain[secret], so secrets land page by page during the first refresh and autonomy is reached before the first sign in finishes rather than at some later point - creating or editing a resource stores the ciphertext it just produced, so an offline read after an edit returns the new secret, never a stale one Offline unlock: - the passphrase is verified locally, so an unreachable server says nothing about whether someone may open their own data; if the account carries a local replica, unlocking continues and the app runs on local data until synchronisation becomes possible again - an account that has never synchronised still needs the server, and still shows the existing "server not reachable" screen Nothing here expires local data, and no server response deletes it. Also lands the M0 groundwork for the delta protocol: docs/sync-protocol.md as the normative cross-platform spec, and a core/sync module with the status model, network state and the documented retry ladder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66Ton99
force-pushed
the
codex/offline-local-replica
branch
from
August 12, 2026 12:52
bfb5b2e to
173a940
Compare
66Ton99
marked this pull request as draft
August 12, 2026 19:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements M0 + M1 of
PLAN.md— the autonomy half of the offline replica work. The server side (passly_api) is untouched; that is M2.Why
Requirement R0: a device that has synchronised once must stay fully usable for an unlimited time, even if the server is switched off for good. Two things stood in the way:
GET /secrets/resource/{id}.json.M1 — autonomy
Local secret store
Secretentity holding the OpenPGP ciphertext exactly as the server sent it, keyed by resource, cascade-deleted with it. Databasev24; the migration is purely additive and needs no network, so an app update applied offline cannot strand an account.SecretInteractoris now local-first: it decrypts the stored ciphertext and only asks the network for a secret this device has never stored, caching whatever comes back. Every consumer benefits — password reveal, TOTP, autofill, passkey, and re-encryption for sharing.contain[secret], so secrets land page by page during the first refresh. Autonomy is reached before first sign-in finishes, and an interrupted refresh still leaves everything it downloaded fully usable offline.Offline unlock (R6)
ServerNotReachableno longer blocks sign-in. The passphrase has already been verified locally at that point, so an unreachable server says nothing about whether someone may open their own data. If the account carries a local replica (HasLocalReplicaUseCase), unlocking continues and the app runs on local data until synchronisation becomes possible again.Nothing added here expires local data, and no server response deletes it.
M0 — groundwork for the delta protocol
docs/sync-protocol.md— normative, cross-platform spec forsync/1: the change journal,seqcursor, watermark, the four endpoints, apply rules, the error table (no error makes local data unavailable), offline write queue and conflict rules, and per-platform storage bindings.core/syncmodule:SyncTrigger,SyncStatus/SyncStatusRepository,SyncResult,NetworkStateProvider, andSyncBackoff(the documented 30s→6h ladder that saturates and never gives up).Verified without code changes
Documented in place so they are not "fixed" backwards later:
EncryptedSharedPreferencesand is not bound to biometrics — re-enrolling a fingerprint cannot destroy the store.GetFeatureFlagsUseCasealready returns the last known snapshot offline, becauseSaveFeatureFlagsUseCasewrites every key.OpenPgp.timeOffsetSecondsdefaults to0, so crypto falls back to device time when the server cannot be asked.RefreshSessionUseCaseis now only reachable for a passkey this device never stored.Testing
./gradlew assembleDebug unitTest— green.SecretInteractorTest(4 cases) pins the core guarantee: a cached secret is decrypted with zero network interaction; a fetched secret is stored for later; a failed store does not fail the read; an unreachable server only matters when there is no local copy.InvalidPackageDeclarationon the 8 new files, which is the repo-wide baseline (2018 pre-existing occurrences fromnet.svaroh.passlypackages living incom/passbolt/mobile/androiddirectories).Reviewer notes
contain[secret]=1makesResourcesIndexControllerwrite aSecretAccessesrow per resource on every full refresh.PLAN.md§4.5 introduces a dedicatedsyncevent type for this — until M2 lands, secret-access reports and notifications will be noisy.PostSignInActionsInteractorcannot fetch feature flags, entry is still blocked by the existingfeatureflagserrorscreen. The "server is gone for good" scenario fails earlier, atServerNotReachable, so it is unaffected — left alone deliberately.Resource.secretModifiedcolumn;Secret.modifiedalready carries it.core/syncsources use directories matching their package rather than the legacycom/passbolt/mobile/androidtree.4.0.0.projectVersionCodeleft at64per.codex/AGENTS.md— say the word if this release needs it raised.migrate23To24, cascade delete) were not run — no device was attached. The migration SQL matches Room's exported24.jsonbyte for byte.🤖 Generated with Claude Code