Files
bitlogin/docs/portable-app-state.md

12 KiB

BitLogin Portable Application State

Status: Experimental design Document version: 0.1 Date: July 22, 2026 Implementation status: Not implemented

1. Summary

Portable Application State allows compatible applications to store encrypted, user-owned settings and lightweight state through BitLogin so the application experience follows the user across devices.

Examples include:

  • Interface preferences
  • Subscription and follow selections
  • Block and mute lists
  • Draft-editor preferences
  • Notification settings
  • Upload defaults
  • Saved views and filters
  • Application-specific bookmarks

The application defines the meaning of its records. BitLogin provides:

  • Namespacing
  • Local encryption and decryption
  • Relay publication and retrieval
  • Independent record replacement
  • Recovery on a clean device
  • Conflict and rollback metadata
  • Export and deletion

Portable Application State must not be stored directly inside the BitLogin credential or recovery capsule.

2. Motivation

A portable Nostr identity does not automatically produce a portable application experience. Users may recover the same npub on another device while losing:

  • Application preferences
  • Locally maintained lists
  • Draft settings
  • Notification choices
  • Saved filters
  • Other application state that was never published as standard Nostr events

This causes decentralized applications to feel less coherent than conventional accounts even when identity recovery works correctly.

The product goal is:

Compatible applications should feel like the same account on every device without placing their private database under a centralized provider.

3. Scope

Portable Application State is intended for small, encrypted, user-specific records that are useful across devices.

Appropriate examples:

BitRoad
├── storefront editor preferences
├── notification settings
└── saved management filters

BitVid
├── playback preferences
├── subscription presentation settings
└── upload defaults

Satisfied
├── analysis display preferences
├── dietary preference settings
└── conversation presentation settings

Inappropriate uses include:

  • Large binary files
  • Video or image storage
  • High-frequency telemetry
  • Server-authoritative balances
  • Payment credentials
  • S3 credentials
  • NWC credentials
  • Cryptocurrency seed phrases
  • Data requiring transactional database semantics

Large files belong in user-selected storage such as S3-compatible services. High-value credentials belong in the Connection Vault.

4. Record model

Application state must be divided into independently replaceable logical records rather than one global application document.

Good:

network.bitroad/preferences/editor
network.bitroad/preferences/notifications
network.bitroad/filters/product-list

Avoid:

network.bitroad/everything

This reduces multi-device clobbering and allows selective deletion, export, and permission management.

5. Application identifiers

Each application must use a stable identifier. Recommended forms:

network.bitroad
network.bitunlock
network.bitvid
app.satisfied

An application identifier must:

  • Be lowercase ASCII.
  • Contain only letters, numbers, dots, and hyphens.
  • Be between 3 and 128 characters.
  • Remain stable across deployments and domains.
  • Not impersonate another application's identifier.

A future registry or signed application manifest may bind an identifier to one or more origins and application pubkeys. Until then, users must be shown the requesting origin whenever an app asks to read or write state.

6. State record identifier

Each record has a logical key chosen by the application:

preferences/editor
preferences/notifications
lists/blocked-pubkeys
views/default-product-filter

The combination of application ID and record key identifies the replaceable record.

Requirements:

  • UTF-8 text normalized according to the final protocol specification.
  • No secrets or personal data embedded in the public identifier.
  • Stable for the logical record.
  • Limited to a reasonable maximum length.

7. Nostr event

Each state record should be stored as its own encrypted NIP-78-style addressable event under the user's everyday Nostr identity or a future application-state identity.

Proposed event:

{
  "pubkey": "<everyday-or-app-state-pubkey>",
  "created_at": 1784750000,
  "kind": 30078,
  "tags": [
    ["d", "bitlogin:app-state:<opaque-record-id>"]
  ],
  "content": "<encoded-encrypted-envelope>",
  "sig": "<signature>"
}

The public d tag should use an opaque deterministic or random identifier so relays do not learn the application ID or record key.

A possible opaque identifier is:

base64url(
  SHA256(
    "bitlogin/app-state-id/v1"
    || 0x00
    || application_id
    || 0x00
    || record_key
  )
)

If deterministic IDs are used, clients must acknowledge that observers who already know the exact application ID and record key may test guesses. A keyed identifier derived from an app-state root provides stronger privacy and is preferred for the final protocol.

8. Encryption key hierarchy

The final design should use a stable application-state root that survives password changes and ordinary identity use.

Two options require evaluation:

  1. Derive app-state keys from a dedicated random root stored in both capsules.
  2. Derive app-state keys from the Connection Vault root with strict domain separation.

The dedicated-root option provides stronger compartmentalization. Reusing the Connection Vault root reduces capsule expansion but couples settings recovery to the higher-value credential vault.

A proposed dedicated construction is:

app_state_prk = HKDF-Extract(
  salt = SHA256("bitlogin/app-state-root/v1"),
  IKM = application_state_root
)

Then derive one key per application and record:

record_key = HKDF-Expand(
  app_state_prk,
  "bitlogin/app-state-record/v1"
    || 0x00
    || application_id
    || 0x00
    || record_key_name,
  32
)

Exact encoding, normalization, and test vectors must be fixed before implementation.

9. Plaintext schema

{
  "schema": "bitlogin.app-state.v1",
  "application_id": "network.bitroad",
  "record_key": "preferences/editor",
  "generation": 7,
  "created_at": 1784700000,
  "updated_at": 1784750000,
  "content_type": "application/json",
  "data": {
    "compactMode": true,
    "defaultCurrency": "sat"
  }
}

Required fields:

  • schema
  • application_id
  • record_key
  • generation
  • created_at
  • updated_at
  • content_type
  • data

Initial supported content type:

application/json

Binary content and arbitrary executable code are prohibited.

10. Encryption and padding

Records should reuse BitLogin's audited primitives where possible:

  • RFC 8785 canonical JSON serialization
  • AES-256-GCM
  • Fresh random 96-bit nonce per publication
  • Context-bound associated data
  • Fixed-size plaintext buckets

Proposed associated data:

bitlogin|app-state|v1|<author-pubkey>|30078|<d-tag>

The app-state specification may require larger buckets than the identity capsules. Relay limits must be checked before publication.

11. Reading and writing

Conceptual application API:

const state = await bitlogin.appState.get({
  applicationId: "network.bitroad",
  recordKey: "preferences/editor"
});

await bitlogin.appState.put({
  applicationId: "network.bitroad",
  recordKey: "preferences/editor",
  data: {
    compactMode: true,
    defaultCurrency: "sat"
  }
});

The host application must not receive keys for other applications' namespaces.

12. Authorization

An application should receive permission to access only its own namespace by default.

Suggested permission screen:

BitRoad wants to:

✓ Read BitRoad settings stored through BitLogin
✓ Update BitRoad settings
✗ Read settings belonging to other applications

[Not now] [Allow]

Optional scopes:

app-state:read
app-state:write
app-state:delete
app-state:export

Permissions should bind to:

  • Application ID
  • Requesting origin
  • Optional application pubkey
  • Allowed record-key prefixes
  • Expiration or persistence choice

13. Conflict handling

Nostr addressable events use last-write-wins replacement. BitLogin should reduce surprise by including:

  • Monotonic generation
  • Monotonic event timestamps
  • Local high-water marks
  • Previous-event ID where useful
  • Optional application-level merge metadata

When two devices update the same record concurrently, BitLogin may:

  1. Accept the newest valid event.
  2. Surface a conflict to the application.
  3. Preserve the losing plaintext locally long enough for user recovery.
  4. Allow the application to merge records and publish a new generation.

BitLogin must not attempt semantic merging of arbitrary application data.

14. Local cache

Decrypted app state may be cached locally for performance, namespaced by:

account -> application -> record

Cache entries must be cleared when:

  • The account logs out.
  • The account is removed from the device.
  • The user deletes the record.
  • The application's permission is revoked, when configured to clear local data.

Applications should receive immutable copies rather than references to shared internal state.

15. Deletion

Deleting a state record should:

  1. Publish an encrypted tombstone for the addressable record.
  2. Publish a NIP-09 deletion request for the prior event where appropriate.
  3. Retain a local deletion high-water mark.
  4. Clear decrypted local cache.
  5. Notify the application.

Deletion from BitLogin does not guarantee every relay or third-party app has deleted prior copies.

16. Export and migration

Users should be able to export:

  • All encrypted app-state events
  • Records for one application
  • One logical record
  • A user-readable decrypted export after explicit confirmation

Encrypted BitLogin recovery exports should include the latest state events and tombstones so the user's app experience can be restored even if relays lose the records.

17. Privacy and security

Application state may reveal sensitive preferences, subscriptions, block lists, health-related choices, or private workflow information.

Implementations must:

  • Encrypt all non-public state locally.
  • Avoid application IDs and record names in public tags where practical.
  • Never include plaintext state in logs or analytics.
  • Prevent one app from reading another app's namespace.
  • Show the requesting account and origin during authorization.
  • Treat HTML, scripts, and executable payloads as untrusted data.
  • Enforce size and nesting limits before parsing.
  • Clear decrypted state when sessions lock or permissions are revoked.

A malicious BitLogin client or compromised device can still read all state available in that session.

18. Acceptance criteria

The feature is complete when:

  1. A compatible app can write at least two independent encrypted records.
  2. A clean device can restore those records after BitLogin login or recovery.
  3. Updating one record does not replace another record.
  4. One application cannot read another application's state through the public API.
  5. Conflicting generations are detected and surfaced.
  6. Deleted records do not silently reappear from stale relay results on a device with a newer high-water mark.
  7. App-state events are included in encrypted account exports.
  8. Switching BitLogin accounts switches the entire visible app-state namespace.
  9. Relay-visible metadata does not expose plaintext application content.
  10. Invalid, oversized, or unsupported records fail closed.
  1. Finalize the key root and opaque record-ID design.
  2. Implement local encrypted records and validation.
  3. Add per-application namespace authorization.
  4. Add relay synchronization and quorum readback.
  5. Add rollback and conflict detection.
  6. Add export, deletion, and account-switch integration.
  7. Publish test vectors and a minimal example application.