Memories / Documentation

On this page

Memories

Android/iOS client for Memories. The Flutter UI calls the shared Rust service through crates/ffi/memories.h; it does not implement a separate storage or cryptography protocol.

Choose a mode

On first launch, including the first launch after upgrading from a version with no mode preference, choose where you want to work:

Mode Data location What you need
Standalone Encrypted library on this phone; works offline Create a local library or unlock the existing one
Center Your self-hosted server's hosted store Server origin such as https://center.example.com and a member token
SaaS The hosted store at https://memories.ivjn.us A member token for that service; the URL is fixed

The app's Center mode means a server running with hosted storage enabled (memories-center --data DIR). It is different from a ticket-only authorization Center, which distributes signed tickets for local devices. Center and SaaS use the same hosted client; they differ in endpoint selection and saved connection profile. Neither requires creating or unlocking a local library first.

First connection

  1. Select a mode. The app remembers the choice for the next launch.
  2. For Standalone, create or unlock your library. For Center, enter the server's HTTPS origin without an API path; for SaaS, keep the fixed service address.
  3. In either hosted mode, enter the member token supplied by your service operator and choose Connect. The member's role and scope determine what it can do; use a member token for content work, not an operator admin token.
  4. After connecting, open Hosted knowledge to browse, search, view, edit or delete knowledge. You can also review requests or open Paired devices. The server address, current vault and granted scope remain visible.

Hosted content is encrypted at rest with an operator-held password, but the operator can read it. The app displays Server can read. Device-to-device sealed secrets have a separate end-to-end protection boundary.

Switch, disconnect and reconnect

Use Switch mode in the main toolbar before or after login. The dialog marks the current mode and explains the effect; select another mode to switch, or Cancel to keep the session. Finish or leave an open editor first; leaving an unsaved draft asks for confirmation.

Action Result
Switch mode Locks the local library, stops sharing, closes the active hosted connection and clears visible content; saves the new mode
Return to Standalone Reopens the local library screen in its locked state; existing data remains
Enter Center or SaaS again Restores that mode's saved connection when available
Disconnect Removes the current profile's saved token and closes its session; the URL and other mode's profile remain
Background the app Locks local access, clears hosted sessions and dismisses sensitive pages and drafts
Return from the background Use Reconnect saved session on the hosted page, or enter a token and connect again

Switching does not upload, copy, merge or delete library data. Disconnecting does not revoke the token on the server. Center and SaaS keep their URL/token pairs in separate platform secure-storage entries. On upgrade, the old hosted connection is imported into the matching mode only; see configuration and migration.

Available features

Workflow Standalone Center / SaaS
Local library creation, backup and restore Available Use the hosted service; no local setup is needed
Browse, search, view, edit and delete knowledge Available after unlock Hosted knowledge, with actions restricted by token scope
Create knowledge cards Local full-page editor Hosted full-page editor with the fields accepted by the hosted API
Review hosted authorization requests Switch to Center or SaaS Pending/history tabs, approve/reject confirmation and refresh
Device pairing Local device/member approval workflow Hosted pairing, sealed-secret and device controls, subject to permissions
Relay sharing and cloud connections Available through local library controls Not exposed on the hosted page

Hosted creation omits tags, source and relationships; editing an existing ordinary card supports these fields and preserves other metadata. Ordinary memories use the text editor. Structured profiles, protected cards, type templates and E2E cards are view-only in this general editor. Both editors retain a draft when saving fails; drafts are not persisted across app locking or backgrounding. See content editing and authorization.

Connection troubleshooting

The app validates tokens through /v1/session before saving a connection, even for write-only accounts. Upgrade self-hosted servers together with the app. Read-only accounts can browse without mutation controls; write-only accounts can create without loading the library or type list. The server authorizes every operation again. Search excludes private content, credentials and files; clear the query to return to the knowledge list. Expired/revoked sessions, insufficient permissions and unavailable servers have localized error feedback. A 401 requires disconnecting and connecting with a valid token.

  • Cannot connect: check the complete HTTPS origin, network, certificate and token. A self-hosted Center must enable hosted storage; a ticket-only service cannot supply this screen. localhost on a phone is the phone itself.
  • Permission denied: check the token's vault, role, scope and expiry with the service operator. Selecting another mode does not grant access.
  • No pending requests: refresh and check the requester/vault/approver routing. The history tab is not a complete server audit log.
  • Session closed after backgrounding: reconnect explicitly. Unsaved drafts have been dismissed as part of locking and cannot be restored.
  • Native service could not start: use a build containing the matching Rust library. Hosted modes also use FFI and require the native library.

Development

Build native libraries first with scripts/build-android.ps1 or scripts/build-ios.sh from the repository root. See the root README for toolchain requirements and signing instructions.

Use Flutter 3.44.2, matching CI. From this directory:

flutter pub get
flutter gen-l10n
dart format --output=none --set-exit-if-changed lib test integration_test
flutter analyze
flutter test
flutter test integration_test/native_smoke_test.dart -d DEVICE_ID
flutter run -d DEVICE_ID

Widget tests use a fake backend to exercise lock-screen lifecycle behavior, mode selection/switching, hosted-session isolation, cards, localization and secret-reference flows. Clipboard tests mock Flutter's platform channel; they do not verify delivery to an OS clipboard. The integration test loads the real native library and creates a disposable database on the device. Normal runs use the platform application-support directory and a Keychain/Keystore-protected local factor. Use test data during development; see SECURITY.md for the current trust boundaries.

Document secrets and clipboard

In Standalone mode, open a memory/card menu and choose Protect secret in document to replace an exact value in its current body with a named reference. Confirm the history warning, enter the reference name and hidden secret, then authorize when prompted. The Rust service stores the credential and replaces the body in one transaction.

Choose Copy secret on a document, protected card or credential to copy its current value after confirmation and authorization. Documents first show a reference picker. OTP cards copy the seed; their separate OTP action generates a one-time code. Values are not displayed, and late responses after backgrounding or locking are discarded. A completed clipboard copy can persist after locking.

Ordinary edits preserve reference mappings. Old document versions and existing backups retain their previous contents. See the wiki secrets guide for reference rules, updates, compatibility and clipboard limits. Rebuild the native library together with the Flutter app when adding these operations.

Localization

Use the language icon in the app bar to select English, Simplified Chinese, Japanese, German, French, or the system default. The preference is stored separately from the encrypted library and is available while locked. Switching languages preserves the current library session.

Translations live in lib/l10n/app_*.arb; flutter gen-l10n produces the ignored lib/l10n/generated/ directory. Material and Cupertino controls use Flutter's localization delegates. Follow the localization guide when adding messages or languages.

Share session (relay RPC host)

In Standalone mode, while unlocked and in the foreground, the overflow menu offers Share session. It joins wss://…/v1/rpc/{room}/a with a high-entropy token so a desktop CLI can attach as slot b (MEMORIES_DAEMON_RELAY / MEMORIES_DAEMON_ROOM / MEMORIES_DAEMON_TOKEN). Backgrounding or locking stops the session. There is no HTTP/WS listen on mobile for MVP; the native FFI denylist still applies. See remote daemon.

Biometric unlock and sensitive actions

In the unlocked Standalone library, use the overflow menu → Unlock with biometrics / device lock (off by default; turning it on requires one system check). lib/biometric.dart wraps local_auth (BiometricPrompt on Android with device PIN/pattern/password fallback; Face ID / Touch ID on iOS, NSFaceIDUsageDescription in Info.plist). MainActivity extends FlutterFragmentActivity as local_auth requires.

  • The library password for quick unlock is stored with AndroidOptions.biometric(resetOnError: false): an Android Keystore AES key with setUserAuthenticationRequired(true) and setInvalidatedByBiometricEnrollment(true). On iOS the Keychain item uses KeychainAccessibility.passcode.
  • Gated: unlocking the library, copying/revealing credential plaintext, using protected cards / OTP, approving pairing and Hosted Center requests, seal and re-seal.
  • When enrollment changes (KeyPermanentlyInvalidatedException) the app tells the user, re-verifies with the device credential, discards only the stale quick-unlock copy, and re-creates the key after one password unlock. The library is never wiped.
  • No biometrics/device credential, or toggle off: the existing password flow.
  • Tests mock DeviceAuth / UnlockSecretStore (test/biometric_test.dart).

Content editor

lib/content_editor.dart is shared by new memories, credentials and Hosted Center entries: file import (title from filename), monospace mode for .env-like content (auto-detected or toggled), and a content-first name that is inferred from the filename or first non-empty line and only prompted for on save when still empty. Handwriting (pen/eraser/undo/clear, ML Kit Digital Ink) and OCR (camera/photos/file, ML Kit text recognition) run on-device; see handwriting and OCR. Widget tests inject ScanServices fakes.

Memories and knowledge cards stay open until saving succeeds. A failed write keeps the draft available for retry; leaving with changes asks for confirmation. Drafts are held in memory only and are still dismissed by background or idle locking. File import asks before replacing existing text. Memories expose their private/ordinary setting before saving, and attachment retries skip files already saved during the current edit.

Knowledge cards use a full-page editor with a persistent save action, custom fields, tags, sources and relationships. Edit content opens the shared text editor for file import, handwriting and OCR. Hosted creation exposes only the metadata accepted by its existing mobile API.

Authorization on mobile

The Center and SaaS pages separate pending requests from history. Review the requester, vault, access scope, duration and full reason before confirming an approval or rejection. Approval still uses the existing device-authentication gate, and the requester must claim the grant separately. Errors preserve the request for retry; pull to refresh or use the refresh button.

The Center request API does not return a complete decision history. The history tab combines decisions made during the current connection with approved requests returned by the Center. These local records are cleared when leaving the page. Failed new connections are closed without replacing the saved profile.

Local member management shows only actions permitted by the displayed roles. Confirmations identify the member and the old/new access. The Rust service rechecks the current policy; UI filtering does not grant any authority. Owner membership cannot be changed through ordinary role actions, and only Owners see ownership transfer.

Brand

Logo source: assets/brand/logo.svg (+ logo-mono.svg, adaptive logo-foreground.svg). Icons and splash images were rendered with rsvg-convert; re-run the same sizes when the logo changes.