New to Claude Skills? Learn how to install them →

Avitorpamplona on GitHub

Account State Management

Free

Manage user-specific states in Amethyst seamlessly.

Get this skill

Free · Opens the source repo

What Account State Management does

The Account State Management skill is designed for developers working with the Amethyst client, specifically focusing on user account states and the associated event store patterns. This skill provides a structured approach to managing per-user state objects, such as follow lists, relay lists, and bookmarks, through the Account.kt class. It helps streamline the interaction between user-specific data and the UI, ensuring that changes in account states are efficiently reflected in the application.

At the core of this skill is the LocalCache, which serves as the central event store for all user notes and metadata. This singleton object maintains a comprehensive cache of Note and User instances, allowing for efficient retrieval and updates. Developers can leverage this skill when adding new account-scoped settings or when deciding whether to query the LocalCache or subscribe to a StateFlow. The skill emphasizes a clear mental model of how account events propagate from the relay to the UI, helping developers understand the flow of data and the importance of state management in the Amethyst architecture.

In addition to managing state, the skill also provides guidance on how to implement new features related to user accounts. By following the outlined steps for adding a new account-scoped setting, developers can easily extend the functionality of the Amethyst client. This includes creating new state classes, managing encrypted lists, and ensuring that all changes are properly reflected in the UI through reactive programming techniques.

Overall, this skill is essential for any developer looking to work with user-specific states in Amethyst, providing the necessary tools and patterns to effectively manage account data and enhance the user experience.

When to use it

Use this skill when developing features that require managing user account states or when interacting with the LocalCache for user data.

When not to use it

This skill may not be suitable for projects outside the Amethyst ecosystem or for tasks not involving user-specific state management.

What you can build with it

Adding a New Account Setting

When you need to implement new features like custom relay lists, this skill provides a clear recipe for adding account-scoped settings.

Managing User Metadata

Use this skill to read and write user metadata effectively, ensuring that all changes are reflected in the UI.

Handling Event Propagation

This skill helps understand how account events cascade from relay arrival to UI state, crucial for responsive applications.

How to install Account State Management

View source

1. Install with the skills CLI

npx skills add vitorpamplona/amethyst/account-state --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by vitorpamplona

Account & Local Cache State

The backbone of Amethyst's client state: one Account per signed-in user, plus the singleton LocalCache that holds every Note and User the client has seen.

When to Use This Skill

  • Working on amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
  • Working on amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
  • Adding a new account-scoped setting (mutes, bookmarks, custom relay lists, private lists)
  • Reading/writing user metadata (User) or note state (Note)
  • Deciding whether to query LocalCache vs subscribe to an Account StateFlow

Mental Model

Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow emits change
                                                            │
                                                            ▼
                                Account state objects pin the relevant addressable notes
                                                            │
                                                            ▼
                            State-object `.flow` updates (kind3FollowList, nip65RelayList, muteList, …)
                                                            │
                                                            ▼
                                                    ViewModels collect
                                                            │
                                                            ▼
                                                 Composables render

LocalCache is the event store. Account is the derived per-user view (follow list, relays, mutes, emojis, bookmarks, etc.). UI listens to the .flow of Account's state objects, not directly to LocalCache, except for note-level rendering.

Key Files

Account.kt (singleton-per-session)

  • class Account(...) — holds 50+ state objects, one per feature, each wired to a specific Nostr kind:
    • kind3FollowList = Kind3FollowListState(...) ← NIP-02 ContactList (kind 3)
    • nip65RelayList = Nip65RelayListState(...) ← NIP-65 RelayList (kind 10002), plus siblings dmRelayList, searchRelayList, blockedRelayList, trustedRelayList, proxyRelayList, broadcastRelayList, indexerRelayList, …
    • muteList = MuteListState(...) ← NIP-51 MuteList (kind 10000)
    • bookmarkState = BookmarkListState(...) ← NIP-51 Bookmarks (kind 10003), plus labeledBookmarkLists, pinState, interestSets, peopleLists, followLists, hashtagList, geohashList, communityList, emoji, blossomServers, …
    • Derived/merged views: hiddenUsers, allFollows, homeRelays, outboxRelays, dmRelays, notificationRelays, trustedRelays, and the live*FollowListsPerRelay outbox loaders.
  • The pattern: each XState class pins its addressable note via cache.getOrCreateAddressableNote(address) (a long-term reference so GC/eviction can't drop it), exposes val flow: StateFlow<…> derived from the note's metadata flow (decrypted through a per-feature DecryptionCache, with backup fallback from AccountSettings, stateIn(scope, Eagerly, …)), and offers suspend mutation helpers (e.g. MuteListState.hideUser(pubkey)) that build the updated signed event. Consumers read account.muteList.flow, never a raw MutableStateFlow on Account.
  • Encrypted lists pair the state object with a DecryptionCache sibling (muteListDecryptionCache, peopleListDecryptionCache, …) so NIP-44 decryption results are cached per event.
  • UI reads via collectAsStateWithLifecycle on Android and collectAsState on Desktop.
  • Sibling files per feature live alongside: AccountSettings.kt, AccountSyncedSettings.kt, plus per-NIP state classes under model/nip02FollowLists/, model/nip51Lists/, model/nip65RelayList/, etc.

LocalCache.kt

  • object LocalCache : ILocalCache, ICacheProvider — the singleton event store.
  • Primary structures (all LargeCache — see nostr-expert/references/large-cache.md):
    • notes: LargeCache<HexKey, Note> — every seen event (regular + addressable + replaceable) keyed by id or d-address.
    • users: LargeCache<HexKey, User> — every seen pubkey, lazily populated.
    • addressables: LargeCache<Address, Note> — secondary index for kind:pubkey:d-tag lookups.
    • channels, deletionIndex, hashtagIndex, …
  • LocalCacheFlow emits coarse-grained "something changed, recheck" signals. Fine-grained reactivity lives in Account's per-kind StateFlows.
  • Eviction is driven by MemoryTrimmingService (android service) under pressure.

Model classes

  • User.kt — mutable profile holder. Contains metadata, follow/follower counts, relay lists, liveset of notes authored.
  • Note.kt — mutable note holder. Contains the underlying Event, replies, reactions, zaps. Mutation via addReply, addReaction, addZap, emitted on Note.flowSet flows.
  • Constants.kt — DEFAULT_RELAYS, magic kinds/limits not covered by quartz.

Adding a New Account-Scoped Setting

Typical recipe:

  1. If the setting is persisted as a Nostr event, pick the right kind (e.g. NIP-51 list, NIP-78 app-specific data, NIP-65 relay list).
  2. Add a model folder under amethyst/.../model/nipXX…/ with an XState class modeled on an existing one (MuteListState for an encrypted list, BookmarkListState for a plain one):
    • Pin the addressable note: val xNote = cache.getOrCreateAddressableNote(XEvent.createAddress(signer.pubKey)).
    • Expose val flow: StateFlow<…> mapped from xNote.flow().metadata.stateFlow, decrypting through a per-feature DecryptionCache if the list is private, with backup fallback from AccountSettings, then stateIn(scope, Eagerly, default).
    • Add suspend mutation helpers that build the updated event via the quartz event class (XEvent.add/remove/create) and return it signed.
  3. In Account.kt, instantiate the state object (and its DecryptionCache sibling if encrypted) as a val. Publishing the returned event goes through Account's send path; the relay subscription side is the relayClient pattern (see relay-client skill).
  4. Add UI that collects account.x.flow. Settings screens live in amethyst/.../ui/screen/loggedIn/settings/.

LocalCache vs Account Flow — Which to Read?

  • Are you rendering a specific note / user you hold an id for?LocalCache.getOrCreateNote(id) + collect note.flowSet.metadata.
  • Are you rendering "my follows", "my mutes", "my relays"?account.<feature>.flow (e.g. account.kind3FollowList.flow, account.muteList.flow, account.nip65RelayList.flow).
  • Are you rendering a feed? → Use a FeedFilter + FeedViewModel (see feed-patterns skill). Don't scan LocalCache in a composable.

Gotchas

  • LocalCache is a singleton across accounts. Switching accounts doesn't wipe it — Account re-derives its flows from the same cache.
  • Don't store Flows inside Note / User expecting them to survive eviction. Eviction drops the whole object.
  • State-object mutation helpers return a signed event — publishing it is the caller's job. A locally updated list without a publish means other clients won't see it.
  • Note is mutable — treat instances as identity-based (same id → same Note). Use .flowSet when you need reactive state.
  • MemoryTrimmingService can evict aggressively on Android under pressure. Don't assume a previously-seen note is still resident.

References

  • references/account-state-flow.md — catalog of major Account state objects and their source kinds.
  • references/local-cache.mdLocalCache internals, insertion path, indexes.
  • Complements: nostr-expert (event parsing), relay-client (subscription wiring), feed-patterns (how feeds consume this state), auth-signers (how mutation signs events).

Frequently asked questions about Account State Management

Similar skills