@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
npm install @flowsta/holochainAgent Linking
linkFlowstaIdentity
Request an identity link from the user's Flowsta Vault:
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):
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:
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:
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:
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
_readmefields 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.
// 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 };
}// 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.
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:
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:
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
- 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.) - Commit the ciphertext as a public entry with a generic
"private"hint (no metadata about content type) - Peers replicate the opaque bytes via gossip — they can see the entry exists but cannot read it
- 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_apidirectly in Rust (see ProofPoll'scrypto.rs) - Electron — Use
lair_keystore_apivia a native Node.js addon, or call lair through its Unix socket - Any backend — Connect to lair's socket and use the
CryptoBoxXSalsaBySignPubKeyrequest
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
| Error | Description |
|---|---|
VaultNotFoundError | Vault not running or not installed |
VaultLockedError | Vault has never been unlocked this session (backups and retrieval work while locked after first unlock) |
UserDeniedError | User rejected the approval dialog |
InvalidClientIdError | Client ID not registered |
MissingClientIdError | No client_id provided |
ApiUnreachableError | Cannot reach Flowsta API |
Function Reference
| Function | Description |
|---|---|
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
- Building Holochain Apps - Step-by-step integration guide
- Agent Linking - How attestations work
- IPC Endpoints - Raw IPC API reference