Skip to content

Identity Switching ​

One Vault, several identities, one unlocked at a time - and one rule for every app that links.

From Flowsta Vault 1.5.0 a person can keep several Flowsta identities in one Vault on one computer. Before that, "this computer" and "this Flowsta identity" were the same thing; now the same computer, the same OS user and the same install of your app can see identity A now and identity B after a switch. Nothing changes in the wire contract. What changes is a question every app has to answer: what is one app agent attached to?

What the switcher is ​

  • The Vault stores each identity in its own partition on disk: its own vault file, keystore, conductor data, linked apps, backups and scopes.
  • One identity is unlocked at a time, and one conductor runs at a time. Switching means locking the current identity and unlocking another; the person picks the identity on the Vault's unlock screen.
  • The Vault remembers which identity it holds even while locked, and counts every change to that in an identity epoch: first setup, a restore of another identity, a switch, and a removal all move it.
  • Removing an identity (a reset) leaves the Vault holding no identity at all until one is created or restored. That is not a switch to someone else: there is nobody to act for.

What your app observes ​

Everything below comes from GET /status on the Vault's IPC server, and the SDK (@flowsta/holochain 3.5.0+) surfaces it through getVaultStatus():

/status fieldSDK fieldMeaning
active_identityactiveIdentityThe agent key of the identity the Vault holds - also while locked. null before any identity is set up or after a removal
agent_pub_key, didagentPubKey, didThe unlocked identity's agent key and Permanent ID; absent while locked
identity_epochidentityEpochCounts every identity change on that computer. A→B→A between two reads is still a change
instance_idinstanceIdIdentifies the answering Vault process; two Vault copies on different ports (two OS users) have different ids
claims-Relay-login claim nonces from the last two minutes, so a web page can tell which of several Vaults on a computer is this person's. Not needed by desktop apps

Every response also carries the header x-flowsta-vault-identity with the agent key the Vault holds. The full endpoint is in the IPC reference.

Requests that name an identity. An app bound to an identity sends expected_identity with its requests - the SDK does this for you after linkFlowstaIdentity, in the POST body of backupToVault, signDocument and authenticateWithVault and in the query string of getFlowstaLinkStatus and listVaultBackups. A Vault holding a different identity refuses with 409 identity_mismatch; a Vault holding none refuses with 409 identity_unconfirmed. The check runs again after any unlock or approval wait, so a switch during a dialog cannot slip through.

What the SDK throws. linkFlowstaIdentity records the identity it linked with (identity binding). From then on backupToVault, signDocument and authenticateWithVault throw IdentityMismatchError when the Vault is unlocked under a definitely different identity, and retrieveFromVault / restoreFromVault throw it when the Vault answers 409. Keys are compared by decoding to bytes, so the base64url and base58 spellings of one key never false-mismatch.

Locked is not a switch. A locked Vault reports unlocked: false and no agent_pub_key; calls fail with VaultLockedError. On Vault 1.5.0 active_identity still names the held identity, so a switch made and then locked is visible; on older Vaults a locked Vault says nothing about who it is.

Noticing a switch. onIdentityChanged(callback) polls /status (there is no push channel to apps) and fires when the identity epoch moves or the held key changes; it is seeded from the bound identity, so an app that opens against a Vault already switched hears about it on the first tick. Correctness never depends on receiving it: every call asserts on its own.

Required for every Holochain app ​

These three hold whichever model you pick below.

  1. One app agent links to one Flowsta identity. The IsSamePersonEntry on your DHT is a public claim that two keys are one person. Linking the same app agent to a second identity would claim that two people are one. The zome refuses it (create_external_link fails when the agent already holds a live link to a different external agent), and the Vault refuses before showing any dialog when another identity in the same Vault already links that agent: 409 agent_linked_elsewhere, with a description naming the identity ("This app is already connected to Name in this Vault. Open the app as that identity, or disconnect it there first."). The SDK surfaces it as a FlowstaHolochainError with code === 'agent_linked_elsewhere'. Linking the same agent to the same identity again is fine (reinstall, restore).
  2. A mismatch is a stop. When the Vault holds a different identity than the one your app linked with, show it ("Your Vault is signed in as someone else") and offer only: switch the Vault back, open the app as the identity the Vault holds now (Model 2), or disconnect. Never catch IdentityMismatchError and call linkFlowstaIdentity again to carry on - that is the double attestation the rule above exists to prevent.
  3. Key data by identity. Anything your app stores for a person is named by partitionKeyFor(agentPubKey) (SDK 3.3.0): the first 16 hex characters of SHA-256 over the agent key's 39 raw bytes, the same for both spellings of a key, and the same key the Vault, ProofPoll and Your Own AI use for their own per-identity folders. The agent key itself never appears in a folder or database name.

Model 1: one agent per install, bound to one identity ​

The SDK 3 default, and correct without changes. Your app has one Holochain agent; linkFlowstaIdentity binds it to the identity it linked with; every write-shaped call refuses under another identity.

What the person experiences. They link your app as identity A and use it. They switch their Vault to B. Your app keeps running, but every Vault-backed action - backups, signing, sign-in - now fails with IdentityMismatchError, and the Vault's own 409 stops anything that goes around the SDK. The app works for nobody until they switch the Vault back to A, or disconnect. If your app also keeps per-identity profiles (Model 2) it can offer "open as this identity" instead; a pure Model 1 app has nothing to offer but those two.

typescript
import {
  getVaultStatus, getBoundIdentity, agentKeysMatch, onIdentityChanged,
} from '@flowsta/holochain';

// On launch: is the Vault holding the identity this install linked with?
const status = await getVaultStatus();
const bound = getBoundIdentity();
const held = status.activeIdentity ?? (status.unlocked ? status.agentPubKey : undefined);
if (bound && held && agentKeysMatch(bound, held) === false) {
  showMismatchBanner(); // switch back, or disconnect - never link again
}

// While running: react before a call refuses (UX only - every call asserts on its own)
const stop = onIdentityChanged(() => {
  showMismatchBanner();
});

Valid for apps that are one-person-per-computer by nature. What Model 1 must never do is the naive path: catch the mismatch, link again, carry on.

Model 2: one agent per identity ​

The app keeps one profile per Flowsta identity, named by partitionKeyFor(agentPubKey): its own agent key store, conductor data, records, link file and binding. At launch it opens the profile of the identity the Vault holds (activeIdentity - readable even while the Vault is locked, so the right profile opens before the unlock). On a switch it stops acting for the old identity and offers Open as this identity - a restart into the other profile - or Disconnect. No entry is ever attested twice, and switching back finds everything where it was.

This is the model for any app that wants a household or a shared computer to work. It needs the app to own its conductor (the Tauri/Electron desktop pattern): a web page talking to a hosted conductor has no profile to switch. ProofPoll 0.4.x and Your Own AI 0.8.x are built this way; both keep a profiles/<partition key>/ folder per identity, poll the Vault's status from their Rust side, pause backups and other Vault-backed work the moment the identity differs, and offer the restart. Nothing is swapped in place: the old identity's profile is left as it was.

What the person experiences. They use the app as A, switch the Vault to B, and the app shows a banner: "Your Vault is signed in as someone else - open the app as this identity, or switch your Vault back." "Open as this identity" restarts the app into B's profile. If B has used this app before, everything of B's is there; if not, B sees a fresh app and the first Vault-backed action asks B to link it (a normal approval dialog in the Vault, under B). Switching the Vault back to A and restarting returns A's data untouched.

The SDK helpers that carry the restart:

typescript
import {
  onIdentityChanged, reconnectIdentity, linkFlowstaIdentity, partitionKeyFor,
} from '@flowsta/holochain';

// 1. Notice the switch (epoch-based on Vault 1.5.0)
const stop = onIdentityChanged(async (next) => {
  pauseVaultBackedWork();               // backups, signing, sign-in
  const folder = await partitionKeyFor(next);
  offerRestartInto(folder);             // "Open as this identity" / "Disconnect"
});

// 2. After the restart, in the profile the app opened (its own agent key):
const outcome = await reconnectIdentity({ clientId, localAgentPubKey: thisProfileAgentKey });
switch (outcome.state) {
  case 'reconnected':
    // This identity already links this app - the binding is restored silently.
    break;
  case 'approval_needed':
    // A stranger to this identity: run the normal ceremony under it.
    await linkFlowstaIdentity({ appName, clientId, localAgentPubKey: thisProfileAgentKey });
    break;
  case 'locked':
  case 'offline':
    // Leave the binding alone; try again when the Vault is back.
    break;
}

reconnectIdentity rebinds only when the Vault says this profile's agent is already linked under the identity it holds - the person consented before. approval_needed means the Vault has no link for that agent under that identity, so the full approval dialog runs. In Model 1 there is no other profile to open, so approval_needed is a stop, not a prompt to link.

Optional ​

None of these change the rule; each is a few lines once the profiles exist.

  • Backups - the Vault stores backups per identity, and backupToVault sends expected_identity, so a profile can only ever write its own identity's backup. restoreFromVault under the wrong identity throws IdentityMismatchError rather than replaying someone else's data.
  • Private data - encrypted entries and local caches live inside the profile folder, so one identity's private data never mixes with another's.
  • Sign It in-app - signDocument (SDK 3.6.0) publishes to the Sign It network when the app is linked under the identity the Vault holds; under a different one it throws IdentityMismatchError before the dialog.
  • Email scope - authenticateWithVault(nonce, { clientId, scopes: ['email'] }) asks for the verified email of the identity signing in.

Next Steps ​

Documentation licensed under CC BY-SA 4.0.