Skip to content

Account Manager ​

The AccountManager class is the core of the library, as its name suggests its used to manage multiple accounts

Account types ​

By default the account manager comes with no account types. you have to manually add them when you create the instance. luckily there is a handy method to add the most common types registerCommonAccountTypes

ts
import { AccountManager, registerCommonAccountTypes, AmberClipboardAccount } from "applesauce-accounts";

// create an account manager instance
const manager = new AccountManager();

// register common account types
registerCommonAccountTypes(manager);

// manually add account type
manager.registerType(AmberClipboardAccount);

Adding and removing accounts ​

ts
import { AccountManager, registerCommonAccountTypes, PrivateKeyAccount } from "applesauce-accounts";

// create an account manager instance
const manager = new AccountManager();

// register common account types
registerCommonAccountTypes(manager);

// subscribe to the active account
manager.active$.subscribe((account) => {
  if (account) console.log(`${account.id} is now active`);
  else console.log("no account is active");

  updateUI();
});

// create an account
const account = PrivateKeyAccount.fromKey("788229e1801c4576391d39a03610293ea7e6645c9d39aca54c62fc6d71cbc385");

// add it to the manager
manager.addAccount(account);

// set it as active
manager.setActive(account);

// later, remove the account and the active account will also update
manager.removeAccount(account.id);

Active account ​

The AccountManager class exposes a set of methods to track which account is active and switch the active account

  • AccountManager.active gets the currently active account
  • AccountManager.active$ an observable of the active account, can be used to subscribe to changes
  • AccountManager.setActive(id: string | Account) set the active account

Persisting accounts ​

The account manager exposes two methods that can be used to persist accounts between app reloads. toJSON and fromJSON

Serialized accounts contain plaintext secrets

Depending on the account type, toJSON can include raw private keys (PrivateKeyAccount), bunker client keys and pairing secrets (NostrConnectAccount), seed words (SeedWordsAccount), or other bearer credentials. Anything you store with it is readable by any script running on the page (XSS, shared browser profiles, synced/unencrypted backups). Prefer PasswordAccount — which stores an NIP-49-encrypted key — or encrypt the serialized JSON yourself before writing it to storage.

ts
import { AccountManager } from "applesauce-accounts";
import { registerCommonAccountTypes } from "applesauce-accounts/accounts";

// create an account manager instance
const manager = new AccountManager();

// register common account types
registerCommonAccountTypes(manager);

// first load all accounts from localStorage
const json = JSON.parse(localStorage.getItem("accounts") || "[]");
await manager.fromJSON(json);

// next, subscribe to any accounts added or removed
manager.accounts$.subscribe((accounts) => {
  // save all the accounts into the "accounts" field
  localStorage.setItem("accounts", JSON.stringify(manager.toJSON()));
});

// load active account from storage
const active = localStorage.getItem("active");
if (active) manager.setActive(active);

// subscribe to active changes
manager.active$.subscribe((account) => {
  if (account) localStorage.setItem("active", account.id);
  else localStorage.clearItem("active");
});

Integration ​

With EventFactory ​

The AccountManager's signer property always points to the active account, so you can pass it straight into factory chains:

ts
const note = NoteFactory.create("Hello").as(manager.signer);

manager.setActive(account1);
await note.sign();

manager.setActive(account2);
await note.sign();

With ActionRunner ​

ts
const actions = new ActionRunner(eventStore, manager.signer, publishFn);

await actions.run(FollowUser, pubkey); // Uses active account

manager.setActive(differentAccount);
await actions.run(FollowUser, pubkey); // Automatically uses new account

With ProxySigner ​

Create custom proxy signers for specific accounts:

ts
const specificSigner = new ProxySigner(
  manager.accounts$.pipe(map((accounts) => accounts.find((a) => a.id === specificId)?.signer)),
);

With React ​

tsx
function AccountSwitcher() {
  const accounts = use$(manager.accounts$);
  const active = use$(manager.active$);

  return (
    <select value={active?.id} onChange={(e) => manager.setActive(e.target.value)}>
      {accounts.map((a) => (
        <option key={a.id} value={a.id}>
          {a.metadata?.name}
        </option>
      ))}
    </select>
  );
}

Best Practices ​

Single Instance ​

ts
// accounts.ts
export const manager = new AccountManager();
registerCommonAccountTypes(manager);

Auto-save Pattern ​

ts
// Load from storage
await manager.fromJSON(JSON.parse(localStorage.getItem("accounts") || "[]"));

// Auto-save on changes
manager.accounts$.subscribe(() => {
  localStorage.setItem("accounts", JSON.stringify(manager.toJSON()));
});

// Save/restore active account
manager.active$.subscribe((account) => {
  if (account) localStorage.setItem("active", account.id);
});

const activeId = localStorage.getItem("active");
if (activeId) manager.setActive(activeId);

Register Types Early ​

ts
// ✅ Good - register before loading
registerCommonAccountTypes(manager);
await manager.fromJSON(saved);

// ❌ Bad - types not registered
await manager.fromJSON(saved);
registerCommonAccountTypes(manager);

Type-Safe Metadata ​

ts
interface AccountMeta {
  name: string;
  color: string;
}

const manager = new AccountManager<AccountMeta>();
const account = PrivateKeyAccount.generateNew<AccountMeta>();
account.metadata = { name: "Alice", color: "#ff0000" };

Error Handling ​

ts
const account = manager.getAccount(accountId);
if (!account) {
  console.error("Account not found");
  return;
}
manager.setActive(account);