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.connectandagent.sessions.createKeyShareper 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
tenantsnamespace.
This page covers the patterns that let one custody platform serve many shapes of customer.
What is runtime-configurable
| Parameter | Surface | Typical change frequency |
|---|---|---|
| Per-tenant cluster URLs | Your config / secret manager → ClusterAgent.connect(host, port, apiKey) | Region expansion, agent rotation |
| Per-tenant hot-key threshold | DKG ceremony parameter at provisioning time | Per-tenant compliance setup |
| Per-tenant cold cohort shape | EmbeddedAgent constructor at cold-wallet provisioning | Per-tenant compliance setup |
| Workflow definitions | Workspace dashboard | Compliance changes, experiments |
| Workflow routing per tenant | Your application → choose which workflow ID to trigger | Per-tenant onboarding, experimentation |
| Velocity / denylist / policy parameters | Workflow component config | Risk-team adjustments |
| Time-lock windows | Workflow component config | Compliance changes |
| Approval pool composition | Workflow component config | Operator onboarding / departure |
| Audit emission target | Your SIEM subscriber configuration | SIEM 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 only — agent.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:
- Provision a new key share via a fresh DKG.
- Build a transfer workflow that drains the old key's balances to the new key's address.
- 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:
- Application receives a hot-tier withdrawal request for tenant
client-9c2f. - App reads
client-9c2ffrom the tenant directory — seesjurisdiction: 'EU',tier: 'enterprise'. - App triggers
tenant.hotWorkflowId(which resolves toenterprise-hot-payout-eu) withtenantIdin the payload. - Workflow's policy components look up tenant-specific velocity limits and denylist via an HTTP call to your application.
- Policy stages run; the operation is in policy, so signing proceeds.
- The signing component invokes the EU cluster at
tenant.hotKeyShareId— the ceremony fans out across EU-region cluster nodes. - Partials return; the signing component calls
Scheme.finalizeSignature(...)locally; the broadcast component sends to the EU-hosted RPC endpoint. - 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
| Setting | Where it lives | Who edits it |
|---|---|---|
| Tenant directory | Your application database | Operations / onboarding team |
| Per-tenant policy parameters | Your application database (read by workflow components at runtime) | Compliance / risk teams |
| Workflow definitions | Workspace dashboard | Platform / operations teams |
| Per-tenant workflow routing | Your application | Operations team |
| Approval pool composition | Your application + workflow component config | Compliance / operations teams |
| Cluster URLs and API keys | Your secret management | Platform / SRE team |
| Self-hosted vs built-in cluster placement | Your environment topology | Platform team |
| Audit emission targets | Your SIEM subscriber configuration | Platform 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
- Hot Wallet — the tier this dynamic config drives in production.
- Cold Wallet — the air-gapped tier under multi-party approval.
- Regulated Design — how multi-tenancy maps to compliance controls.
- Embedded Wallet — Dynamic Configuration — the consumer-side counterpart.
Regulated Design
How a Zafeguard custody whitelabel maps to the controls regulators expect — threshold MPC, documented approval flows, segregation of duties, audit emission across SDK + workflow execution + cluster audit logs, key sovereignty via self-hosted clusters, and right-to-revoke.
Smart Account — Lazy Deploy + Gas Sponsorship
Create a Gnosis Safe smart account per user that auto-deploys on first use, with a relayer MPC key sponsoring all gas costs.