Skip to content

Holochain Signing Service

An optional service that signs Holochain actions on behalf of your users.

The Flowsta Signing Service allows applications to request cryptographic signatures using users' Flowsta agent keys. This is useful for Holochain apps that want to share a common identity across apps, and for non-Holochain apps that want cryptographic signing capabilities.

Private Keys Never Leave Flowsta

Your application never receives users' private signing keys. When you request a signature, Flowsta's Holochain conductor signs the data and returns only the signature. The private keys remain securely stored on Flowsta's infrastructure and are never exposed through the API.

This Service is Optional

Holochain developers can still use their own agent keys and Holochain infrastructure. The signing service is an additional option, not a requirement.

Why Use the Signing Service?

For Holochain Developers

BenefitDescription
Shared IdentityUsers have the same agent key across all participating apps
No Conductor SetupDon't need to run your own Holochain conductor
Built-in ConsentUsers explicitly grant signing permission
Audit TrailUsers can see which apps have signed on their behalf

For Non-Holochain Developers

BenefitDescription
Cryptographic SigningSign arbitrary data with user's agent key
Verify AuthenticityProve data was created by a specific user
Audit TrailsCreate cryptographically verifiable records
No Crypto ExpertiseFlowsta handles the key management

How It Works

The signing service acts as a secure intermediary. Your app sends data to be signed, Flowsta's Holochain conductor performs the signing using the user's private key, and only the signature is returned. Private keys never leave the conductor.

mermaid
sequenceDiagram
    participant User
    participant YourApp as Your App
    participant Flowsta as Flowsta Auth
    participant HC as Holochain Conductor

    User->>YourApp: Uses your app
    YourApp->>Flowsta: Request signing with OAuth token
    Flowsta->>Flowsta: Check signing permission
    Flowsta->>HC: Sign action with user's agent key
    Note over HC: Private key stays here
    HC->>Flowsta: Return signature only
    Flowsta->>YourApp: Return signature + public key
    Note over YourApp: Never receives private key
    YourApp->>User: Action completed!

What You Receive

DataReturnedDescription
Signature✅ YesCryptographic proof the user signed the data
Public Key✅ YesIdentifies which user signed (can be shared publicly)
Private Key❌ NeverRemains secure on Flowsta's Holochain conductor

Installation

bash
npm install @flowsta/auth @flowsta/holochain

Quick Start

1. Create an OAuth Application

  1. Go to dev.flowsta.com
  2. Create a new application
  3. Enable the holochain:sign scope in Holochain Integration settings
  4. Add your redirect URIs
  5. Note your client ID

2. Set Up Authentication with Signing Scope

typescript
import { FlowstaAuth } from '@flowsta/auth';
import { FlowstaHolochain, createHolochainClient } from '@flowsta/holochain';

// Configure auth with holochain:sign scope
const auth = new FlowstaAuth({
  clientId: 'your-client-id',
  redirectUri: 'https://yourapp.com/callback',
  scopes: ['openid', 'public_key', 'holochain:sign'] // Request signing permission
});

// Create Holochain client
const holochain = createHolochainClient(auth);

3. Authenticate Users

typescript
// Redirect to login (user will see consent screen with signing permission)
auth.login();

// Handle callback
const user = await auth.handleCallback();

console.log('User agent key:', user.agentPubKey);
console.log('User DID:', user.did);

4. Sign Holochain Actions

typescript
try {
  const result = await holochain.signAction({
    action: {
      type: 'CreateEntry',
      entry_type: 'message',
      payload: {
        content: 'Hello, Holochain!',
        timestamp: Date.now()
      }
    },
    reason: 'Creating a new message'
  });

  console.log('Signed action:', result.signedAction);
  console.log('Signature:', result.signature);
  console.log('Agent key:', result.agentPubKey);
} catch (error) {
  if (error instanceof ConsentRequired) {
    // User hasn't granted signing permission
    // Re-authenticate with holochain:sign scope
    auth.login();
  }
}

OAuth Scopes

When using the signing service, request these scopes:

ScopeDescriptionRequired
openidUser identifier✅ Yes
public_keyUser's Holochain agent public keyRecommended
holochain:signPermission to sign Holochain actions✅ Yes
didUser's W3C DIDOptional

Important

The holochain:sign scope triggers a special consent screen that clearly explains to users that your app will be able to sign Holochain actions on their behalf. Make sure your application name and logo are set up correctly in the developer portal.

API Reference

signAction(request)

Sign a Holochain action.

Request:

typescript
interface SignActionRequest {
  /** The Holochain action to sign */
  action: Record<string, unknown>;
  /** Human-readable reason (shown in audit log) */
  reason?: string;
}

Response:

typescript
interface SignActionResponse {
  success: boolean;
  signedAction: Record<string, unknown>;
  signature: string;        // Base64-encoded signature
  agentPubKey: string;      // User's agent public key
  did: string;              // User's DID
  signedAt: string;         // ISO timestamp
}

Example:

typescript
const result = await holochain.signAction({
  action: {
    type: 'CreateEntry',
    entry_type: 'post',
    payload: { title: 'Hello World', content: '...' }
  },
  reason: 'Publishing a blog post'
});

signBytes(request)

Sign raw bytes for custom operations. This is useful for non-Holochain apps.

Request:

typescript
interface SignBytesRequest {
  /** Base64-encoded bytes to sign */
  bytes: string;
  /** Human-readable reason */
  reason?: string;
}

Response:

typescript
interface SignBytesResponse {
  success: boolean;
  signature: string;        // Base64-encoded signature
  agentPubKey: string;      // User's agent public key
}

Example:

typescript
// Encode your data as base64
const bytesToSign = btoa(JSON.stringify(myCustomData));

const result = await holochain.signBytes({
  bytes: bytesToSign,
  reason: 'Signing custom data'
});

getPermissions()

Get all apps the user has granted signing permission to.

typescript
const permissions = await holochain.getPermissions();

// Returns:
// [{
//   id: 'uuid',
//   appId: 'uuid',
//   appName: 'My App',
//   appLogo: 'https://...',
//   scopes: ['holochain:sign'],
//   grantedAt: '2025-01-15T10:00:00Z',
//   lastUsedAt: '2025-01-15T10:30:00Z',
//   signCount: 42
// }]

revokePermission(appId)

Revoke signing permission for an app.

typescript
await holochain.revokePermission('app-uuid');

hasSigningPermission()

Check if the user has granted signing permission to your app.

typescript
const hasPermission = await holochain.hasSigningPermission();
if (!hasPermission) {
  // Prompt user to grant permission
}

Error Handling

ConsentRequired

Thrown when the user hasn't granted holochain:sign permission.

typescript
import { ConsentRequired, FlowstaHolochainError } from '@flowsta/holochain';

try {
  await holochain.signAction({ action: myAction });
} catch (error) {
  if (error instanceof ConsentRequired) {
    // Re-authenticate with holochain:sign scope
    console.log('Required scope:', error.requiredScope);
    auth.login();
  } else if (error instanceof FlowstaHolochainError) {
    console.error('Error code:', error.code);
    console.error('Description:', error.description);
  }
}

Common Error Codes

CodeDescription
consent_requiredUser must grant holochain:sign permission
unauthorizedAccess token missing or invalid
insufficient_scopeToken doesn't have holochain:sign scope
signing_failedHolochain conductor error
server_errorInternal server error

User Experience

When users authenticate with the holochain:sign scope, they see a clear consent screen that:

  • Shows your app's name and logo
  • Clearly explains what "Sign Holochain Actions" means
  • Highlights this as a sensitive permission (displayed with amber warning styling)
  • Links to your privacy policy

TIP

Make sure your app's name and logo are configured in the Developer Portal for the best user experience.

User Dashboard

Users can manage signing permissions from their Flowsta dashboard:

  • View all apps with signing permission
  • See signing activity (when, what was signed)
  • Revoke permissions at any time

Non-Holochain Usage

If you're not building a Holochain app but want cryptographic signing:

typescript
import { FlowstaAuth } from '@flowsta/auth';
import { createHolochainClient } from '@flowsta/holochain';

const auth = new FlowstaAuth({
  clientId: 'your-client-id',
  redirectUri: 'https://yourapp.com/callback',
  scopes: ['openid', 'holochain:sign']
});

const signer = createHolochainClient(auth);

// Sign any data
const data = { documentId: '123', approvedBy: user.id, timestamp: Date.now() };
const dataBytes = btoa(JSON.stringify(data));

const result = await signer.signBytes({
  bytes: dataBytes,
  reason: 'Approving document #123'
});

// Store the signature alongside your data
const signedDocument = {
  ...data,
  signature: result.signature,
  signerPubKey: result.agentPubKey
};

Use Cases for Non-Holochain Apps

  • Document signing - Prove a user approved a document
  • Audit logs - Create cryptographically verifiable records
  • Data integrity - Sign data to detect tampering
  • Multi-party workflows - Collect multiple signatures

Security Model

The signing service is designed with security as the top priority:

Private Keys Are Never Exposed

┌─────────────────────────────────────────────────────────────────┐
│                     FLOWSTA INFRASTRUCTURE                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │               Holochain Conductor                        │    │
│  │                                                          │    │
│  │   🔐 Private Keys (stored securely, never transmitted)   │    │
│  │                                                          │    │
│  └─────────────────────────────────────────────────────────┘    │
│                            │                                     │
│                    Signs data internally                         │
│                            │                                     │
│                            ▼                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                    Auth API                              │    │
│  │         Returns: signature + public key only             │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

                    HTTPS Response


┌─────────────────────────────────────────────────────────────────┐
│                      YOUR APPLICATION                            │
│                                                                  │
│   ✅ Receives: signature, public key, timestamp                  │
│   ❌ Never receives: private key                                 │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Trust Model

PartyAccess Level
UserOwns the key, grants/revokes signing permission
FlowstaCustodian of keys, executes signing on user request
Your AppReceives signatures only, cannot sign without user consent

User Controls

Users maintain full control over their signing permissions:

  • View all apps with signing permission in their dashboard
  • See detailed signing activity logs
  • Revoke permissions instantly at any time
  • Receive consent warnings before granting holochain:sign

Security Best Practices

1. Request Only What You Need

Only request holochain:sign if your app actually needs to sign actions. For read-only apps, public_key is sufficient.

2. Provide Reasons

Always include a reason when signing:

typescript
await holochain.signAction({
  action: myAction,
  reason: 'Publishing comment on post #123' // Shown in user's audit log
});

3. Handle Errors Gracefully

typescript
try {
  await holochain.signAction({ action });
} catch (error) {
  if (error instanceof ConsentRequired) {
    // Politely ask user to grant permission
    showModal('This action requires signing permission. Click to authorize.');
  }
}

4. Respect Token Expiration

Access tokens expire. Check before signing:

typescript
if (!auth.isAuthenticated()) {
  auth.login();
  return;
}

await holochain.signAction({ action });

Migration from Direct Holochain

If you're migrating from a direct Holochain integration:

Before (Direct Holochain):

typescript
import { AppWebsocket } from '@holochain/client';

const client = await AppWebsocket.connect('ws://localhost:8888');
await client.callZome({
  cell_id: myCellId,
  zome_name: 'my_zome',
  fn_name: 'create_entry',
  payload: data
});

After (Flowsta Signing Service):

typescript
import { FlowstaAuth } from '@flowsta/auth';
import { createHolochainClient } from '@flowsta/holochain';

const auth = new FlowstaAuth({ clientId, redirectUri, scopes: ['openid', 'holochain:sign'] });
const holochain = createHolochainClient(auth);

// User authenticates via OAuth
auth.login();

// Later, sign actions
const signed = await holochain.signAction({ action: data });

Need Help?

Documentation licensed under CC BY-SA 4.0.