Skip to main content
Multi-tenancy allows a single Credo agent instance to manage multiple isolated tenants, each with their own wallet, storage, and configuration.

Overview

Multi-tenancy is useful for:
  • SaaS platforms - One agent serving multiple customers
  • Enterprise deployments - Separate contexts for departments or users
  • Mediator services - Managing multiple edge agents
  • Wallet-as-a-Service - Providing wallet infrastructure
Each tenant has:
  • Isolated storage and wallet
  • Independent configuration
  • Separate DIDs and credentials
  • Own connection records

Installation

npm install @credo-ts/tenants

Basic Setup

Create a root agent with the TenantsModule:
import { Agent } from '@credo-ts/core'
import { TenantsModule } from '@credo-ts/tenants'
import { AskarModule } from '@credo-ts/askar'
import { agentDependencies } from '@credo-ts/node'
import { askar } from '@openwallet-foundation/askar-nodejs'

const rootAgent = new Agent({
  config: {
    logger: new ConsoleLogger(LogLevel.info),
  },
  dependencies: agentDependencies,
  modules: {
    // Root storage
    askar: new AskarModule({
      askar,
      store: {
        id: 'root-agent',
        key: 'root-agent-key',
      },
    }),
    // Enable multi-tenancy
    tenants: new TenantsModule(),
  },
})

await rootAgent.initialize()

Creating Tenants

1
Create a Tenant
2
Create a new tenant with isolated storage:
3
const tenantRecord = await rootAgent.modules.tenants.createTenant({
  config: {
    label: 'Alice Tenant',
  },
})

console.log('Tenant ID:', tenantRecord.id)
console.log('Tenant Label:', tenantRecord.config.label)
4
Get a Tenant Agent
5
Access the tenant’s agent instance:
6
const tenantAgent = await rootAgent.modules.tenants.getTenantAgent({
  tenantId: tenantRecord.id,
})

// Use the tenant agent like a normal agent
const connections = await tenantAgent.connections.getAll()

// Always end the session when done
await tenantAgent.endSession()
7
Use withTenantAgent
8
Automatic session management:
9
await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: tenantRecord.id },
  async (tenantAgent) => {
    // Work with the tenant agent
    const invitation = await tenantAgent.oob.createInvitation()
    return invitation
  }
)
// Session automatically ended

Tenant Configuration

Configure individual tenants:
const tenantRecord = await rootAgent.modules.tenants.createTenant({
  config: {
    label: 'Corporate Tenant',
    // Tenant-specific logger level
    logger: new ConsoleLogger(LogLevel.debug),
  },
})

Managing Tenants

Get Tenant by ID

const tenant = await rootAgent.modules.tenants.getTenantById(tenantId)
console.log('Tenant:', tenant.config.label)

Find Tenants by Label

const tenants = await rootAgent.modules.tenants.findTenantsByLabel('Alice')

Get All Tenants

const allTenants = await rootAgent.modules.tenants.getAllTenants()
console.log('Total tenants:', allTenants.length)

Query Tenants

const tenants = await rootAgent.modules.tenants.findTenantsByQuery(
  {
    config: {
      label: 'Corporate Tenant',
    },
  },
  {
    limit: 10,
  }
)

Update Tenant

const tenant = await rootAgent.modules.tenants.getTenantById(tenantId)

tenant.config.label = 'Updated Label'

await rootAgent.modules.tenants.updateTenant(tenant)

Delete Tenant

await rootAgent.modules.tenants.deleteTenantById(tenantId)
Deleting a tenant permanently removes all its data, including credentials, connections, and DIDs.

Tenant Agent Usage

Once you have a tenant agent, use it like any other Credo agent:

DIDComm Operations

await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: tenantRecord.id },
  async (tenantAgent) => {
    // Create connection invitation
    const invitation = await tenantAgent.didcomm.oob.createInvitation()

    // Accept credential offer
    await tenantAgent.didcomm.credentials.acceptOffer({
      credentialExchangeRecordId: credentialRecord.id,
    })

    // Request proof
    await tenantAgent.didcomm.proofs.requestProof({
      connectionId: connectionRecord.id,
      protocolVersion: 'v2',
      proofFormats: { /* ... */ },
    })
  }
)

OpenID4VC Operations

await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: tenantRecord.id },
  async (tenantAgent) => {
    // Resolve credential offer
    const offer = await tenantAgent.openid4vc.holder.resolveCredentialOffer(
      credentialOfferUri
    )

    // Request credentials
    const credentials = await tenantAgent.openid4vc.holder.requestCredentials({
      resolvedCredentialOffer: offer,
      credentialConfigurationIds: ['UniversityDegree'],
      // ...
    })
  }
)

AnonCreds Operations

await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: tenantRecord.id },
  async (tenantAgent) => {
    // Create link secret (once per tenant)
    await tenantAgent.modules.anoncreds.createLinkSecret()

    // Get credentials
    const credentials = await tenantAgent.modules.anoncreds.getCredentials()
  }
)

Tenant Modules Configuration

Share module configuration across all tenants:
import { DidCommModule } from '@credo-ts/didcomm'
import { AnonCredsModule } from '@credo-ts/anoncreds'

const rootAgent = new Agent({
  config: {},
  dependencies: agentDependencies,
  modules: {
    askar: new AskarModule({
      askar,
      store: { id: 'root', key: 'root-key' },
    }),
    tenants: new TenantsModule(),
    // These modules are available to all tenants
    didcomm: new DidCommModule({
      endpoints: ['http://localhost:3000'],
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry()],
      anoncreds,
    }),
  },
})
All tenants inherit the module configuration but maintain isolated data.

Storage Updates

Check for Outdated Storage

const outdatedTenants = await rootAgent.modules.tenants.getTenantsWithOutdatedStorage()

for (const tenant of outdatedTenants) {
  console.log(`Tenant ${tenant.id} needs storage update`)
}

Update Tenant Storage

await rootAgent.modules.tenants.updateTenantStorage({
  tenantId: tenantRecord.id,
  updateOptions: {
    // Update options
  },
})

Auto-Update on Startup

Enable automatic storage updates:
const tenantRecord = await rootAgent.modules.tenants.createTenant({
  config: {
    label: 'Alice',
    autoUpdateStorageOnStartup: true,
  },
})

Session Management

Manual Session Control

const tenantAgent = await rootAgent.modules.tenants.getTenantAgent({
  tenantId: tenantRecord.id,
})

try {
  // Perform operations
  const connections = await tenantAgent.didcomm.connections.getAll()
} finally {
  // Always end the session
  await tenantAgent.endSession()
}

Automatic Session Management

// Preferred: automatic session cleanup
const result = await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: tenantRecord.id },
  async (tenantAgent) => {
    return await tenantAgent.didcomm.connections.getAll()
  }
)
Always end tenant sessions to free resources. Use withTenantAgent for automatic cleanup.

Complete Example

Here’s a complete multi-tenant setup:
import { Agent, ConsoleLogger, LogLevel } from '@credo-ts/core'
import { TenantsModule } from '@credo-ts/tenants'
import { AskarModule } from '@credo-ts/askar'
import { DidCommModule } from '@credo-ts/didcomm'
import { AnonCredsModule } from '@credo-ts/anoncreds'
import { agentDependencies } from '@credo-ts/node'
import { askar } from '@openwallet-foundation/askar-nodejs'
import { anoncreds } from '@hyperledger/anoncreds-nodejs'

// Create root agent
const rootAgent = new Agent({
  config: {
    label: 'Multi-Tenant Root Agent',
    logger: new ConsoleLogger(LogLevel.info),
  },
  dependencies: agentDependencies,
  modules: {
    askar: new AskarModule({
      askar,
      store: {
        id: 'root-agent',
        key: 'root-secure-key',
      },
    }),
    tenants: new TenantsModule(),
    didcomm: new DidCommModule({
      endpoints: ['http://localhost:3000'],
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry()],
      anoncreds,
    }),
  },
})

await rootAgent.initialize()
console.log('Root agent initialized')

// Create tenants
const aliceTenant = await rootAgent.modules.tenants.createTenant({
  config: { label: 'Alice Corporation' },
})

const bobTenant = await rootAgent.modules.tenants.createTenant({
  config: { label: 'Bob Enterprises' },
})

console.log('Tenants created:', aliceTenant.id, bobTenant.id)

// Work with Alice's tenant
await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: aliceTenant.id },
  async (aliceAgent) => {
    const invitation = await aliceAgent.didcomm.oob.createInvitation()
    console.log('Alice invitation:', invitation.outOfBandInvitation.toUrl({
      domain: 'http://localhost:3000',
    }))
  }
)

// Work with Bob's tenant
await rootAgent.modules.tenants.withTenantAgent(
  { tenantId: bobTenant.id },
  async (bobAgent) => {
    await bobAgent.modules.anoncreds.createLinkSecret()
    console.log('Bob link secret created')
  }
)

// List all tenants
const allTenants = await rootAgent.modules.tenants.getAllTenants()
console.log('Total tenants:', allTenants.length)

Routing and Mediation

For multi-tenant mediator setups:
import { DidCommMediatorModule } from '@credo-ts/didcomm'

const rootAgent = new Agent({
  config: {},
  dependencies: agentDependencies,
  modules: {
    tenants: new TenantsModule(),
    didcomm: new DidCommModule({
      mediator: new DidCommMediatorModule({
        autoAcceptMediationRequests: true,
      }),
    }),
  },
})
Each tenant can act as a mediation recipient to the root agent’s mediator.

Best Practices

// Good: automatic cleanup
await rootAgent.modules.tenants.withTenantAgent({ tenantId }, async (agent) => {
  await agent.didcomm.connections.getAll()
})

// Risky: manual cleanup required
const agent = await rootAgent.modules.tenants.getTenantAgent({ tenantId })
try {
  await agent.didcomm.connections.getAll()
} finally {
  await agent.endSession() // Don't forget!
}
Never share credentials, connections, or DIDs between tenants. Each tenant should operate independently.
Use meaningful labels and maintain a mapping between your application’s user IDs and tenant IDs.
Regularly check for outdated tenant storage:
const outdated = await rootAgent.modules.tenants.getTenantsWithOutdatedStorage()
for (const tenant of outdated) {
  await rootAgent.modules.tenants.updateTenantStorage({ tenantId: tenant.id })
}

Next Steps

Agent Config

Configure tenant-specific settings

DIDComm

Use DIDComm with multi-tenant agents

AnonCreds

Issue credentials per tenant

API Reference

Full Tenants API documentation

Build docs developers (and LLMs) love