Skip to content

@flowsta/holochain

SDK for integrating Holochain apps with Flowsta Vault.

@flowsta/holochain provides functions for agent identity linking, Vault communication, and CAL-compliant backups. It wraps Flowsta Vault's IPC endpoints into a simple TypeScript API.

Installation

bash
npm install @flowsta/holochain

Agent Linking

linkFlowstaIdentity

Request an identity link from the user's Flowsta Vault:

typescript
import { linkFlowstaIdentity } from '@flowsta/holochain';

const result = await linkFlowstaIdentity({
  appName: 'ChessChain',
  clientId: 'flowsta_app_abc123',
  localAgentPubKey: myAgentKey,   // uhCAk... format
});

// Commit to your DHT
await appWebsocket.callZome({
  role_name: 'my-role',
  zome_name: 'agent_linking',
  fn_name: 'create_direct_link',
  payload: {
    other_agent: decodeHashFromBase64(result.payload.vaultAgentPubKey),
    other_signature: base64ToSignature(result.payload.vaultSignature),
  },
});

getFlowstaIdentity

Query linked agents on your DHT. Returns an array of linked agent public keys (as raw bytes):

typescript
import { getFlowstaIdentity } from '@flowsta/holochain';

const linkedAgents = await getFlowstaIdentity({
  appWebsocket,
  roleName: 'my-role',
  agentPubKey: someAgentKey, // Uint8Array from @holochain/client
});

// linkedAgents is Uint8Array[] - array of linked agent public keys
if (linkedAgents.length > 0) {
  console.log(`Linked to ${linkedAgents.length} Flowsta identities`);
}

getVaultStatus

Check if Vault is running and unlocked:

typescript
import { getVaultStatus } from '@flowsta/holochain';

const status = await getVaultStatus();
// { unlocked: boolean, version?: string, agentPubKey?: string }

revokeFlowstaIdentity

Notify Vault that a link has been revoked. Best-effort - if Vault is not running, returns { success: false } without throwing:

typescript
import { revokeFlowstaIdentity } from '@flowsta/holochain';

await revokeFlowstaIdentity({
  appName: 'ChessChain',
  localAgentPubKey: myAgentKey, // uhCAk... format
});

checkFlowstaLinkStatus

Check if Vault still considers an agent linked. Returns { linked: false } if Vault is not running:

typescript
import { checkFlowstaLinkStatus } from '@flowsta/holochain';

const status = await checkFlowstaLinkStatus({
  clientId: 'flowsta_app_abc123',
  localAgentPubKey: myAgentKey, // uhCAk... format
});

if (status.linked) {
  console.log('App name:', status.appName);
}

Backups

Flowsta Vault provides encrypted local storage for app data backups. Users can view, export, and delete their backups from the Vault's Your Data page at any time.

Backups work while the Vault is locked

As of @flowsta/holochain v2.1.0, backups can be stored and retrieved even when the Vault is locked — as long as it has been unlocked at least once in the current session. This means your app can back up data in the background without interrupting the user.

Including private data in backups

If your app stores encrypted entries on the DHT (see Encrypted Entries on Public DHT below), include the decrypted content in your backup. The backup is already protected by Vault's own encryption — there's no need to double-encrypt.

Every time you add new entry types or zome functions that create user data, update your getData function to include that data. If you don't, users will have incomplete exports.

Structure your backup for human readability:

  • Include _readme fields explaining each section
  • Use human-readable names (poll_title, voted_for) not just hashes
  • Group private data separately with a note that it was decrypted for the export
  • Include context so entries make sense standalone (e.g. a vote rationale should include what was voted on)

What to back up

Under the Cryptographic Autonomy License (CAL), users must be able to get a copy of their own data along with the cryptographic keys needed to use it independently.

Only back up data the user created or owns. Do not back up the entire DHT — that includes other people's data and can grow very large. Your getData function should query for records authored by the current agent only.

typescript
// Good: only the user's own data
getData: async () => {
  const myPubKey = appWebsocket.myPubKey;

  // Fetch user's own records from zome calls
  const myGames = await getMyGames(myPubKey);       // games I created
  const myMoves = await getMyMoves(myPubKey);        // moves I made
  const mySettings = await getMySettings();           // my preferences

  return { games: myGames, moves: myMoves, settings: mySettings };
}
typescript
// Bad: backing up everything on the DHT
getData: async () => {
  const allGames = await getAllGames();   // includes other people's games
  const allMoves = await getAllMoves();   // includes other people's moves
  return { games: allGames, moves: allMoves };
}

Designing your zomes for backup

Add zome functions that return records filtered by author. For example, get_my_games(agent_pub_key) rather than relying solely on get_all_games(). This makes it easy to back up only the user's data and keeps backups small.

startAutoBackup

Start automatic backups to Vault's encrypted local storage. Each backup is saved as a separate timestamped snapshot (up to 10 per app — oldest are auto-rotated). Works while the Vault is locked.

typescript
import { startAutoBackup } from '@flowsta/holochain';

const stopBackup = startAutoBackup({
  clientId: 'flowsta_app_abc123',
  appName: 'ChessChain',
  getData: async () => {
    const myPubKey = appWebsocket.myPubKey;
    const myGames = await getMyGames(myPubKey);
    const myMoves = await getMyMoves(myPubKey);
    return { games: myGames, moves: myMoves };
  },
  intervalMinutes: 60, // Default: 60
  onSuccess: (result) => console.log('Backup saved:', result.label),
  onError: (err) => console.error('Backup failed:', err),
});

// Stop when your app closes
stopBackup();

backupToVault

Trigger a manual backup. Omit label to create a new timestamped snapshot, or pass a label to overwrite a specific named backup:

typescript
import { backupToVault } from '@flowsta/holochain';

// Auto-versioned snapshot (recommended)
const result = await backupToVault(
  { clientId: 'flowsta_app_abc123', appName: 'ChessChain' },
  { games: myGames, moves: myMoves },
);

// Named backup (overwrites same label each time)
await backupToVault(
  { clientId: 'flowsta_app_abc123', appName: 'ChessChain', label: 'pre-migration' },
  { games: myGames, moves: myMoves },
);

retrieveFromVault

Retrieve a stored backup:

typescript
import { retrieveFromVault } from '@flowsta/holochain';

const data = await retrieveFromVault({
  clientId: 'flowsta_app_abc123',
  label: 'latest',
});

Encrypted Entries on Public DHT

Holochain apps can store private data on the public DHT by encrypting entries client-side before committing them. The encrypted blob is replicated across peers for resilience (survives device loss), but only the author can decrypt it.

How it works

  1. Encrypt in your app backend using the agent's lair-managed keys (crypto_box_xsalsa_by_sign_pub_key — lair converts Ed25519 to x25519 internally). Works with any framework that can connect to lair (Tauri, Electron, Node.js, etc.)
  2. Commit the ciphertext as a public entry with a generic "private" hint (no metadata about content type)
  3. Peers replicate the opaque bytes via gossip — they can see the entry exists but cannot read it
  4. Decrypt when reading — only the author's lair private key can open the crypto_box

What peers see

cipher: [187, 202, 33, ...]  (opaque bytes, xsalsa20poly1305)
nonce:  [244, 219, 96, ...]  (24 bytes, random)
hint:   "private"             (no content type metadata)

Key properties

  • 256-bit security — XSalsa20-Poly1305 with X25519 key exchange
  • Tied to Holochain identity — uses the agent's lair-managed keys, not a separate password
  • DHT is the backup — data survives device loss because peers replicate the ciphertext
  • Future-ready for sharing — X25519 naturally supports encrypting to other agents (not just self)

Framework support

The encryption happens via lair-keystore's client API (lair_keystore_api crate in Rust, or any language that can speak lair's protocol). Any framework that manages a local Holochain conductor can use this pattern:

  • Tauri — Use lair_keystore_api directly in Rust (see ProofPoll's crypto.rs)
  • Electron — Use lair_keystore_api via a native Node.js addon, or call lair through its Unix socket
  • Any backend — Connect to lair's socket and use the CryptoBoxXSalsaBySignPubKey request

Reference implementation

ProofPoll demonstrates this pattern with vote rationales (private notes on votes) and draft polls (encrypted until published). See ProofPoll's crypto.rs, EncryptedEntry type, and the encrypted entry Tauri commands.

Error Types

ErrorDescription
VaultNotFoundErrorVault not running or not installed
VaultLockedErrorVault has never been unlocked this session (backups and retrieval work while locked after first unlock)
UserDeniedErrorUser rejected the approval dialog
InvalidClientIdErrorClient ID not registered
MissingClientIdErrorNo client_id provided
ApiUnreachableErrorCannot reach Flowsta API

Function Reference

FunctionDescription
linkFlowstaIdentity(options)Request identity link from Vault
getFlowstaIdentity(options)Query linked agents on DHT
getVaultStatus(ipcUrl?)Check Vault status
revokeFlowstaIdentity(options)Notify Vault of revocation
checkFlowstaLinkStatus(options)Check link status in Vault
startAutoBackup(options)Start automatic backups
backupToVault(options, data)Store data in Vault
retrieveFromVault(options)Retrieve stored backup
listVaultBackups(ipcUrl?)List all backups in Vault

Next Steps

Documentation licensed under CC BY-SA 4.0.