Custody Whitelabel Solution

Dynamic Configuration

Runtime configuration for a custody whitelabel — how multi-tenant policy lives in your application + the workflow canvas, how SDK constructors handle per-tenant cohort shape, and chain support extension.

Dynamic Configuration

A custody product serves more than one shape of customer. A single institutional treasury has different policy than a custody-as-a-service operator serving thirty end clients. An enterprise client onboarded last year is on a different threshold than the client onboarded last week. A new jurisdiction expansion demands stricter policy for users in that jurisdiction.

The platform separates the moving parts cleanly:

  • Per-tenant cohort and threshold live in your application's constructor logic — your code resolves which agents and threshold to pass to ClusterAgent.connect and agent.sessions.createKeyShare per tenant.
  • Per-tenant policy lives on the workflow canvas — the workflow that wraps signing contains your policy components, and your application triggers the right workflow ID for the right tenant.
  • Runtime tenant directory lives in your own database, not in a platform-managed tenants namespace.

This page covers the patterns that let one custody platform serve many shapes of customer.


What is runtime-configurable

ParameterSurfaceTypical change frequency
Per-tenant cluster URLsYour config / secret manager → ClusterAgent.connect(host, port, apiKey)Region expansion, agent rotation
Per-tenant hot-key thresholdDKG ceremony parameter at provisioning timePer-tenant compliance setup
Per-tenant cold cohort shapeEmbeddedAgent constructor at cold-wallet provisioningPer-tenant compliance setup
Workflow definitionsWorkspace dashboardCompliance changes, experiments
Workflow routing per tenantYour application → choose which workflow ID to triggerPer-tenant onboarding, experimentation
Velocity / denylist / policy parametersWorkflow component configRisk-team adjustments
Time-lock windowsWorkflow component configCompliance changes
Approval pool compositionWorkflow component configOperator onboarding / departure
Audit emission targetYour SIEM subscriber configurationSIEM migration

The platform does not ship per-tenant primitives in the SDK — workspace.tenants.create, workspace.policies.update, workspace.signers.reshape don't exist as APIs. Multi-tenancy is something you build at your application layer using the SDK's per-key primitives and the workflow canvas.


The multi-tenant shape

A custody-as-a-service operator runs one Zafeguard workspace and serves many end clients. The pattern: model tenants in your database, hold their per-tenant configuration there, and resolve cohort + workflow at runtime.

interface Tenant {
  id: string;                                   // your application-scoped tenant ID
  name: string;
  jurisdiction: 'US' | 'EU' | 'AP';
  tier: 'starter' | 'enterprise';
  hotKeyShareId: string;                        // the cluster's key share ID after DKG
  coldSignerId: string;                         // signerId for the cold EmbeddedAgent
  approvalPool: { officerIds: string[]; quorum: number };
  hotWorkflowId: string;                        // workflow ID for in-policy operations
  coldWorkflowId: string;                       // workflow ID for above-threshold cold ops
}

async function loadTenant(id: string): Promise<Tenant> {
  return db.tenants.find(id);
}

The platform-side surfaces — workflows, the cluster, the SDK — operate on the IDs you store. The "tenant" abstraction lives entirely in your application.

Provisioning a new tenant

Onboarding a new end client is a multi-step ceremony orchestrated by your application:

import { ClusterAgent, EmbeddedAgent, Curve, RecoveryKind } from '@zafeguard/mpc-sdk';

async function onboardTenant(tenantInput: TenantInput): Promise<Tenant> {
  // 1. Provision the hot-wallet key share on the cluster.
  const agent = ClusterAgent.connect(HOT_CLUSTER_HOST, 443, process.env.ZG_AGENT_KEY!, true);
  const dkg = await agent.sessions.createKeyShare({
    curve: 'secp256k1',
    threshold: tenantInput.tier === 'enterprise' ? 3 : 2,
    // peers, etc — per the SDK reference
  });
  const dkgResult = await dkg.promise();

  // 2. Schedule the cold-wallet online ceremony.
  //    Requires officers present — typically not a synchronous call;
  //    your application schedules the ceremony slot, your operations team runs it,
  //    and the resulting signerId + publicKey are recorded back into your DB.

  // 3. Persist the tenant record.
  const tenant = await db.tenants.create({
    id: tenantInput.id,
    name: tenantInput.name,
    jurisdiction: tenantInput.jurisdiction,
    tier: tenantInput.tier,
    hotKeyShareId: dkgResult.sessionId,
    coldSignerId: tenantInput.coldSignerIdReservation,
    approvalPool: tenantInput.approvalPool,
    hotWorkflowId: hotWorkflowIdForTier(tenantInput.tier),
    coldWorkflowId: coldWorkflowIdForJurisdiction(tenantInput.jurisdiction),
  });

  // 4. Configure tenant-specific policy parameters in your DB (read by your
  //    workflow components at execution time via the tenant ID in the request payload).
  await db.policy.set(tenant.id, {
    velocityLimits: tenantInput.velocityLimits,
    timeLockSeconds: tenantInput.timeLockSeconds,
    aboveThresholdEscalationUsd: tenantInput.aboveThresholdEscalationUsd,
  });

  return tenant;
}

There is no single platform API call that does all of this. Onboarding is a procedure your application implements, composed from the SDK calls that exist (DKG ceremony, signer creation) plus your own state management.


Per-tenant workflow routing

Your application triggers a workflow per tenant request. Two patterns work for routing:

Pattern A — One workflow ID per tier/jurisdiction shape. Pre-create a small number of workflow definitions on the canvas (e.g. enterprise-hot-payout-us, enterprise-hot-payout-eu, starter-hot-payout) and route to the right one based on the tenant.

async function triggerHotPayout(tenant: Tenant, request: PayoutRequest) {
  return fetch(
    `https://api.zafeguard.com/v1/sdk/workflows/${tenant.hotWorkflowId}`,
    {
      method: 'POST',
      headers: { 'X-Api-Key': process.env.ZAFEGUARD_API_KEY! },
      body: JSON.stringify({
        tenantId: tenant.id,                  // your workflow components read this
        keyShareId: tenant.hotKeyShareId,
        ...request,
      }),
    },
  );
}

Pattern B — One workflow with tenant-aware policy components. A single workflow whose policy components read the tenant ID from the payload and apply tenant-specific limits looked up via an HTTP call to your application:

HOT_PAYOUT (one workflow, tenant-routed at runtime)
        |
        v
   Tenant policy lookup       (HTTP call to your app: GET /tenants/:id/policy)
        |
        v
   Apply velocity limit       (from looked-up policy)
        |
        v
   Apply denylist             (from looked-up policy)
        |
        v
   Sign                       (against tenant.hotKeyShareId)
        |
        v
   Submit + audit

The right pattern depends on how different your tenants' policy shapes are. Heterogeneous tenants tend toward pattern A (clear workflows you can audit per tier); largely-similar tenants tend toward pattern B (one workflow, less canvas duplication).


Policy reconfiguration

Policy changes are workflow-canvas edits — open the workflow, update the component config (new velocity limit, new time-lock window), save. The new policy applies on the next workflow execution.

Two practical implications:

  • In-flight workflows finish under the policy that was in force when they started. Updating the velocity component does not retroactively reject a workflow that's currently in its time-lock window.
  • The workflow's version history is the policy change log. Comparing two workflow versions shows the diff of what changed.

The platform does not currently ship a policies.update({ effectiveAt, approvalGate }) SDK call. Approval gating and effective-at scheduling are layered on top: your application maintains the canonical policy change request, gates the dashboard edit behind your operating procedure, and audits the change in your own log.


Cohort reshape

Cryptographically meaningful changes (threshold, cohort party set, curve) require ceremony work. The SDK's reshare is same-cohort onlyagent.sessions.createReshare(...) and loaded.reshare({ passphrase }) rotate share material within the existing cohort shape; they do not let you add or remove parties.

Real cohort-shape changes mean:

  1. Provision a new key share via a fresh DKG.
  2. Build a transfer workflow that drains the old key's balances to the new key's address.
  3. Mark the old key retired in your tenant directory.

This is a coordinated operations event. Your tenant records hold both the old and new hotKeyShareId during the migration window; routing flips to the new ID once the transfer settles.


Per-region deployment

For tenants with data-residency requirements, your tenant directory holds the region; your ClusterAgent.connect(...) resolver picks the regional cluster URL; your workflow definitions route to regionally-pinned chain RPC components and audit-emission targets.

function clusterForJurisdiction(jurisdiction: Tenant['jurisdiction']) {
  switch (jurisdiction) {
    case 'US': return { host: 'hot-cluster-us.your-custody.com',  apiKey: process.env.ZG_HOT_US! };
    case 'EU': return { host: 'hot-cluster-eu.your-custody.com',  apiKey: process.env.ZG_HOT_EU! };
    case 'AP': return { host: 'hot-cluster-ap.your-custody.com',  apiKey: process.env.ZG_HOT_AP! };
  }
}

async function connectForTenant(tenant: Tenant): Promise<ClusterAgent> {
  const { host, apiKey } = clusterForJurisdiction(tenant.jurisdiction);
  return ClusterAgent.connect(host, 443, apiKey, true);
}

Application code that's tenant-aware reads the jurisdiction; the rest stays tenant-agnostic.


Chain support extension

Adding a new chain to an existing custody operation:

Same-curve chain (almost free)

A new EVM L2 or sidechain: add the chain's chainId and a JSON-RPC endpoint to workspace configuration, use the existing or new chain components in your workflows, route requests against the existing secp256k1 key. No DKG, no ceremony.

Different-curve chain

Adding a chain on a new curve (e.g. Solana to an existing Bitcoin+EVM operation) requires a new key share per tenant on the cluster. For the hot tier:

const dkg = await agent.sessions.createKeyShare({
  curve: 'ed25519',
  threshold: 2,
  // peers — typically the same cohort
});
const dkgResult = await dkg.promise();
await db.tenants.update(tenant.id, { solanaHotKeyShareId: dkgResult.sessionId });

For the cold tier, this is a coordinated operations event — bring the cold device online for a new ceremony with the cohort, schedule it alongside a planned pool replenishment.


Putting it together

A complete picture of one hot-tier signing operation moving through the configuration stack:

  1. Application receives a hot-tier withdrawal request for tenant client-9c2f.
  2. App reads client-9c2f from the tenant directory — sees jurisdiction: 'EU', tier: 'enterprise'.
  3. App triggers tenant.hotWorkflowId (which resolves to enterprise-hot-payout-eu) with tenantId in the payload.
  4. Workflow's policy components look up tenant-specific velocity limits and denylist via an HTTP call to your application.
  5. Policy stages run; the operation is in policy, so signing proceeds.
  6. The signing component invokes the EU cluster at tenant.hotKeyShareId — the ceremony fans out across EU-region cluster nodes.
  7. Partials return; the signing component calls Scheme.finalizeSignature(...) locally; the broadcast component sends to the EU-hosted RPC endpoint.
  8. Your SIEM subscriber consumes the workflow execution stream and writes the audit event to the EU SIEM target.

Every routing decision is data in your database or configuration in your workflow definitions. Application code is one tenant lookup and one workflow trigger. The flexibility lives at your application layer plus the canvas; the simplicity lives in your codebase.


Where configuration lives

SettingWhere it livesWho edits it
Tenant directoryYour application databaseOperations / onboarding team
Per-tenant policy parametersYour application database (read by workflow components at runtime)Compliance / risk teams
Workflow definitionsWorkspace dashboardPlatform / operations teams
Per-tenant workflow routingYour applicationOperations team
Approval pool compositionYour application + workflow component configCompliance / operations teams
Cluster URLs and API keysYour secret managementPlatform / SRE team
Self-hosted vs built-in cluster placementYour environment topologyPlatform team
Audit emission targetsYour SIEM subscriber configurationPlatform team

The pattern that emerges: the SDK is your tool for cryptographic operations; the workflow canvas is your tool for policy and orchestration; everything else is application-layer state you own.


Next

On this page