Skip to content

Desktop App Authentication ​

Identity for desktop apps through Flowsta Vault - your app never touches a credential.

Desktop apps (Tauri, Electron, or anything that can reach localhost) talk to the user's Flowsta Vault over local IPC on 127.0.0.1, ports 27777-27779 (the Vault takes the next port up when 27777 is held; the SDK resolves it for you - pass ipcUrl to pin it). The user sets up and unlocks their Vault in the Vault app itself - your app only ever observes Vault state and asks it to sign things. Your app never sees a password; the user's Vault key signs for them.

How It Works ​

Unlike web OAuth, there's no browser redirect. Your app calls the Vault's local IPC; the Vault shows the user exactly what your app is asking for and signs with keys that never leave the device.

Desktop app sign-in: your desktop app requests a one-time challenge from the Flowsta API, sends it to Flowsta Vault on 127.0.0.1:27777-27779 over local IPC, the user approves and the Vault signs it, and the app exchanges the signature for a session token

Your app ──local IPC──▶ Flowsta Vault ──signature──▶ your app
                          (user approves in Vault UI)

::: caution Native (non-webview) IPC clients If your app calls the Vault from native code (Rust reqwest, Go, Python, …) instead of a webview, you must send an Origin header manually on every request - browsers add it automatically, native HTTP clients don't. See the Origin-header caution in the IPC reference. :::

Quick Start ​

1. Install the SDK ​

bash
npm install @flowsta/holochain

2. Check Vault Status ​

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

const status = await getVaultStatus();
if (status.blocked) {
  showMessage('Your browser blocked access to Flowsta Vault - allow local network access');
  return;
}
if (!status.running) {
  showMessage('Please install and open Flowsta Vault');
  return;
}
if (!status.unlocked) {
  showMessage('Please unlock Flowsta Vault to continue');
  return;
}

console.log('Permanent ID:', status.did);          // did:flowsta:… (SDK 3.6.0)
console.log('Agent key:', status.agentPubKey);
console.log('Identity held:', status.activeIdentity); // also reported while locked (Vault 1.5.0, SDK 3.5.0)
// After the user has linked your app, and with the display_name /
// profile_picture / username scopes selected at dev.flowsta.com:
console.log('Name:', status.displayName);

The user unlocks the Vault in the Vault's own window - never in yours. status.activeIdentity names the identity the Vault holds even while locked, status.identityEpoch counts every identity change on that computer, and status.instanceId identifies the answering Vault process - see Identity switching for what to do with them.

3. Sign the user in ​

Your backend issues a random nonce, the Vault signs it, and your backend verifies the signature and issues its own session. The nonce must not start with flowsta-: that prefix is reserved for Flowsta's own protocols and the Vault refuses to sign it for any origin outside https://*.flowsta.com (400 reserved_prefix). The /auth/vault/challenge and /auth/vault/token endpoints of the Flowsta API belong to that first-party flow - they mint a Flowsta session, not yours, so do not build on them.

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

// 1. Your backend issues a one-time random nonce (never a "flowsta-…" string)
const { nonce } = await fetch('https://api.yourapp.com/auth/nonce', { method: 'POST' })
  .then((r) => r.json());

// 2. The Vault signs it - the user approves in the Vault window
const { signature, agentPubKey, did } = await authenticateWithVault(nonce, {
  appName: 'YourApp',
  clientId: YOUR_CLIENT_ID, // from dev.flowsta.com
});

// 3. Your backend verifies the signature and issues its own session
const { token } = await fetch('https://api.yourapp.com/auth/vault', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ nonce, signature, agent_pub_key: agentPubKey, did }),
}).then((r) => r.json());

The Vault shows an Authentication Request dialog with your app name, the requesting origin, and the reason you pass (default "Sign in"); the user can tick "Remember this site" to auto-approve next time. It signs the nonce's UTF-8 bytes with the identity's Ed25519 device key and returns the signature as standard base64, the agent key as uhCAk…, and the Permanent ID. The user has about 60 seconds to answer (timeout otherwise); if the Vault is locked when the request arrives, it brings its unlock screen forward and holds the request for about 55 seconds first.

On your backend, verify with the public key inside the agent key. The agent key is u + base64url of 39 bytes: a 3-byte prefix, the 32-byte Ed25519 public key, and a 4-byte checksum. Check that the nonce is one you issued, unused, and recent; then:

javascript
// Node.js backend - the same check the Flowsta API runs
import nacl from 'tweetnacl';

const keyBytes = Buffer.from(agentPubKey.slice(1), 'base64url'); // 39 bytes
const publicKey = keyBytes.subarray(3, 35);                       // 32-byte Ed25519 key

const valid = nacl.sign.detached.verify(
  new TextEncoder().encode(nonce),   // the Vault signed the nonce's UTF-8 bytes
  Buffer.from(signature, 'base64'),  // 64 bytes, standard base64
  publicKey,
);

A valid signature proves the person holds that identity's device key. Key your user record by the agent key (or the DID, which embeds it) and issue your session. With scopes: ['email'] (SDK 3.2.0, Vault 1.3.0+) the dialog also offers the user's verified email address and the result carries email + emailVerified: true if they allow it.

What else the Vault does for your app ​

CapabilitySDK functionDocs
Link your app's Holochain agent to the user's identitylinkFlowstaIdentity()Agent Linking
Sign documents/files with the user's identity (a linked app publishes to the Sign It network)signDocument()Sign It Developer Guide
Automatic encrypted backups of your app's datastartAutoBackup()Backups
Restore after reinstallrestoreFromVault() (replay) or seed adoption (shared DHT)Reinstall recovery
Notice an identity switch in the VaultonIdentityChanged(), reconnectIdentity()Identity switching

Open your web app signed in ​

SDK 3.9.0, Vault 1.6.1+, @flowsta/login-button 0.2.0+

When your desktop app sends the person to your web app (billing, an account page, a dashboard), the browser does not have to sign in again, and never has to reach the Vault itself. openWebSignedIn() runs the web sign-in from the desktop app: a challenge for the web client, signed by the Vault over loopback (silently when the person's trust setting for your app allows it, otherwise one dialog), exchanged for the OAuth code, and opened in the browser as your callback URL. The PKCE verifier and state travel in the URL fragment, which the browser never sends to any server.

Two registrations are involved: your desktop app (Holochain type) and your web app (OAuth type), both in your organization. Name the web app as the desktop app's companion in the portal (how); the Vault only signs a web-login challenge for an app's own client or its declared companion.

typescript
import { openWebSignedIn } from '@flowsta/holochain';
import { openUrl } from '@tauri-apps/plugin-opener';

const { url, consentRequired } = await openWebSignedIn({
  clientId: WEB_CLIENT_ID,                           // the companion web app
  redirectUri: 'https://app.example.com/auth/callback/', // one of its registered redirect URIs, exactly
  scopes: ['openid', 'display_name'],
  appName: 'YourApp',
  openUrl,                                            // opens the finished URL in the OS browser
});
// consentRequired: true the first time the API wants the person to see the
// consent page for the web client (not first-party, not yet authorized) -
// `url` is then the consent page and the callback follows it.

On the web side, handleCallback() from @flowsta/login-button 0.2.0 adopts the fragment and your callback exchanges the code as usual (details). The identity's agent key is left in sessionStorage['flowsta_handoff_agent_pub_key'] for one use, for example to link the device to the person's plan without asking again.

Say who signed in. Any link that signs a browser in also lets a crafted link sign it in as someone else; the one check a person can make is the name on the page. Your first signed-in page should read, for example, "Signed in from YourApp on this computer as Alex." If openWebSignedIn throws (Vault locked or closed, declined, offline), fall back to opening the web app signed out; the person signs in there with the button.

What the person sees, and the copy that goes with each state, is on Sign-in UX.

Error Handling ​

typescript
import {
  FlowstaHolochainError,
  VaultNotFoundError,
  VaultBlockedError,
  VaultLockedError,
  UserDeniedError,
  IdentityMismatchError,
} from '@flowsta/holochain';

try {
  const result = await authenticateWithVault(nonce, { appName: 'YourApp' });
} catch (error) {
  if (error instanceof VaultBlockedError) {
    showMessage('The browser blocked access to Flowsta Vault - allow local network access for this site');
  } else if (error instanceof VaultNotFoundError) {
    showMessage('Flowsta Vault is not running - please open it');
  } else if (error instanceof VaultLockedError) {
    showMessage('Please unlock Flowsta Vault');
  } else if (error instanceof UserDeniedError) {
    showMessage('Sign-in was declined in the Vault');
  } else if (error instanceof IdentityMismatchError) {
    // The Vault holds a different identity than the one this app linked
    // with. Stop here: offer "switch your Vault back", "open as this
    // identity" (a per-identity profile) or "disconnect". Never link again.
    showMessage('Your Vault is signed in as someone else');
  } else if (error instanceof FlowstaHolochainError && error.code === 'timeout') {
    showMessage('No answer from the Vault within 60 seconds');
  }
}

Web vs Desktop Auth ​

FeatureWeb (@flowsta/auth)Desktop (@flowsta/holochain)
Auth methodBrowser redirect to login.flowsta.comLocal Vault IPC
User approvalOAuth consent screenDirectly in the Vault UI
Credentials in your appNeverNever
Client secretNot needed (PKCE)Not needed (local signatures)
Requires VaultYes - on this computer, or with relay login on another device the user ownsYes

Next Steps ​

Documentation licensed under CC BY-SA 4.0.