Understanding Multi-Tenancy
Multi-tenancy in Credo allows you to:- Run multiple isolated agent instances within a single application
- Each tenant has its own wallet, DIDs, and credentials
- Share infrastructure and reduce resource overhead
- Ideal for SaaS applications serving multiple organizations
Basic Multi-Tenant Setup
import { Agent, CacheModule, InMemoryLruCache } from '@credo-ts/core'
import { DidCommModule } from '@credo-ts/didcomm'
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: {
label: 'Multi-Tenant Root Agent',
},
dependencies: agentDependencies,
modules: {
didcomm: new DidCommModule({
endpoints: ['http://localhost:3000'],
connections: {
autoAcceptConnections: true,
},
}),
// TenantsModule enables multi-tenancy
tenants: new TenantsModule(),
// Askar with storage disabled (tenants manage their own storage)
askar: new AskarModule({
askar,
enableStorage: false,
store: {
id: 'root-agent',
key: 'root-agent-key',
},
}),
// Cache for better performance
cache: new CacheModule({
cache: new InMemoryLruCache({ limit: 500 }),
}),
},
})
await rootAgent.initialize()
console.log('✓ Root agent initialized')
// Create first tenant
const tenant1Record = await rootAgent.modules.tenants.createTenant({
config: {
label: 'Acme Corporation',
},
})
console.log('Tenant 1 created:', tenant1Record.id)
// Create second tenant
const tenant2Record = await rootAgent.modules.tenants.createTenant({
config: {
label: 'Globex Industries',
},
})
console.log('Tenant 2 created:', tenant2Record.id)
// Get tenant agent
const tenantAgent = await rootAgent.modules.tenants.getTenantAgent({
tenantId: tenant1Record.id,
})
console.log('Tenant agent label:', tenantAgent.config.label)
// Output: 'Acme Corporation'
// Create a DID for this tenant
const didResult = await tenantAgent.dids.create({
method: 'key',
})
console.log('Tenant DID created:', didResult.didState.did)
// Always end the session when done
await tenantAgent.endSession()
const result = await rootAgent.modules.tenants.withTenantAgent(
{ tenantId: tenant1Record.id },
async (tenantAgent) => {
// Create a connection invitation
const outOfBand = await tenantAgent.didcomm.oob.createInvitation()
return {
invitationUrl: outOfBand.outOfBandInvitation.toUrl({
domain: 'https://acme.example.com',
}),
outOfBandId: outOfBand.id,
}
}
)
console.log('Invitation created:', result.invitationUrl)
// Session automatically closed after callback completes
Multi-Tenant Connections
Establish connections between tenants:// Tenant 1 creates an invitation
const invitation = await rootAgent.modules.tenants.withTenantAgent(
{ tenantId: tenant1Record.id },
async (tenant1Agent) => {
const outOfBand = await tenant1Agent.didcomm.oob.createInvitation({
label: 'Acme Corp Connection',
})
return outOfBand.outOfBandInvitation.toUrl({
domain: 'https://acme.example.com',
})
}
)
console.log('Tenant 1 invitation:', invitation)
// Tenant 2 accepts the invitation
const connectionRecord = await rootAgent.modules.tenants.withTenantAgent(
{ tenantId: tenant2Record.id },
async (tenant2Agent) => {
const { connectionRecord } = await tenant2Agent.didcomm.oob.receiveInvitationFromUrl(
invitation,
{ label: 'Globex Industries' }
)
if (!connectionRecord) {
throw new Error('No connection record created')
}
// Wait for connection to complete
return await tenant2Agent.didcomm.connections.returnWhenIsConnected(
connectionRecord.id
)
}
)
console.log('✓ Connection established between tenants')
// Tenant 1 issues a credential to Tenant 2
await rootAgent.modules.tenants.withTenantAgent(
{ tenantId: tenant1Record.id },
async (issuerAgent) => {
// Get the connection record
const connections = await issuerAgent.didcomm.connections.getAll()
const connection = connections[0]
// Offer credential
await issuerAgent.didcomm.credentials.offerCredential({
connectionId: connection.id,
protocolVersion: 'v2',
credentialFormats: {
anoncreds: {
credentialDefinitionId: 'credDefId123',
attributes: [
{ name: 'company', value: 'Acme Corporation' },
{ name: 'role', value: 'Partner' },
],
},
},
})
console.log('✓ Credential offer sent from Tenant 1')
}
)
// Tenant 2 receives and accepts the credential
await rootAgent.modules.tenants.withTenantAgent(
{ tenantId: tenant2Record.id },
async (holderAgent) => {
// Credential is auto-accepted if configured
const credentials = await holderAgent.didcomm.credentials.getAll()
console.log('✓ Tenant 2 received credentials:', credentials.length)
}
)
Tenant Management
Find and List Tenants
Update Tenant Configuration
Delete a Tenant
Advanced Multi-Tenant Scenarios
Tenant with Custom Modules
Configure tenants with specific module configurations:Tenant-Specific Endpoints
Route messages to specific tenants:Handling Tenant Sessions
Manage multiple concurrent tenant sessions:Storage Considerations
Askar Multi-Tenant Profiles
Use Askar profiles for efficient tenant isolation:Best Practices
- Always call
endSession()or usewithTenantAgent()to prevent resource leaks - Use the cache module to improve performance with many tenants
- Implement proper error handling for tenant operations
- Consider rate limiting per tenant to prevent abuse
- Monitor tenant storage usage in production
- Use tenant-specific logging for debugging
- Implement tenant authentication/authorization in your API layer
- Backup tenant data regularly
Security Considerations
- Each tenant’s wallet is encrypted with its own key
- Tenants cannot access each other’s data
- Root agent should not be directly exposed to end users
- Implement proper tenant ID validation
- Use HTTPS for all tenant endpoints
- Rotate tenant keys periodically
Next Steps
- Learn about Issuing Credentials
- Explore Verifying Presentations
- Understand Connection Establishment