Skip to main content
Version: 0.8 (next)

Mediation

A mediator is an agent that receives messages on behalf of agents that can't be reached directly, and holds them until they are picked up. This page covers running a mediator, connecting an agent to it as a recipient, choosing a pickup strategy, and how routing keys stay in sync.

When you need a mediator​

An agent needs a mediator when other agents can't reach it. A mobile wallet always needs one when it uses DIDComm: a phone has no public address, and the app is often closed. A server agent with public endpoints doesn't need one.

A wallet with a mediator puts the mediator's endpoint and routing key in its invitations and DID documents. Senders wrap their messages in a forward message to the mediator, and the mediator delivers them to the wallet over a WebSocket, or queues them until the wallet picks them up.

Credo implements Coordinate Mediation 1.0 and message pickup (Pickup 1.0 and 2.0) for both roles. Messages from a wallet to other agents don't go through its mediator.

Run a mediator​

A mediator is a Node.js agent with the mediator protocol turned on. It needs an HTTP endpoint for senders and a WebSocket endpoint so wallets can receive messages live.

mediator.ts
import { Agent } from '@credo-ts/core'
import {
DidCommBasicMessageEventTypes,
DidCommBasicMessageRole,
type DidCommBasicMessageStateChangedEvent,
DidCommHttpOutboundTransport,
type DidCommKeylistUpdatedEvent,
DidCommMediatorPickupStrategy,
DidCommModule,
DidCommRoutingEventTypes,
DidCommWsOutboundTransport,
} from '@credo-ts/didcomm'
import { agentDependencies, DidCommHttpInboundTransport, DidCommWsInboundTransport } from '@credo-ts/node'

const mediator = new Agent({
dependencies: agentDependencies,
modules: {
...mediatorStorage, // Storage and key management, see Agent setup
didcomm: new DidCommModule({
endpoints: ['http://localhost:4040', 'ws://localhost:4041'],
transports: {
inbound: [new DidCommHttpInboundTransport({ port: 4040 }), new DidCommWsInboundTransport({ port: 4041 })],
outbound: [new DidCommHttpOutboundTransport(), new DidCommWsOutboundTransport()],
},
connections: { autoAcceptConnections: true },
mediator: { autoAcceptMediationRequests: true },
// A mediator only routes messages, so it doesn't need the other protocols
mediationRecipient: false,
credentials: false,
proofs: false,
basicMessages: false,
}),
},
})

await mediator.initialize()

const mediatorOutOfBandRecord = await mediator.didcomm.oob.createInvitation({
label: 'Mediator',
multiUseInvitation: true,
})
const mediatorInvitationUrl = mediatorOutOfBandRecord.outOfBandInvitation.toUrl({
domain: 'http://localhost:4040/invite',
})

The mediator shares one multi-use invitation with all wallets. Without autoAcceptMediationRequests, listen for DidCommRoutingEventTypes.MediationStateChanged and grant requests in the requested state with agent.didcomm.mediator.grantRequestedMediation(mediationRecordId).

OptionDefaultDescription
mediator.autoAcceptMediationRequestsfalseGrant every mediation request.
mediator.messageForwardingStrategyDirectDeliveryDirectDelivery sends a forwarded message right away over an open session with the recipient, and queues it if that isn't possible. QueueOnly always queues. QueueAndLiveModeDelivery queues, then delivers the queue to recipients in live mode.
messagePickup.maximumBatchSize10Maximum number of messages per pickup batch.
queueTransportRepositoryin memoryWhere queued messages are stored. Pass a persistent implementation in production, or messages are lost on restart.

For production, look at DIDComm Mediator Credo, a mediator built on Credo with a persistent message queue and a Docker image.

Connect to a mediator​

On the recipient, set mediatorInvitationUrl in the mediationRecipient options:

wallet.ts
const alice = new Agent({
dependencies: agentDependencies,
modules: {
...aliceStorage, // Storage and key management, see Agent setup
didcomm: new DidCommModule({
transports: {
outbound: [new DidCommWsOutboundTransport(), new DidCommHttpOutboundTransport()],
},
connections: { autoAcceptConnections: true },
mediationRecipient: {
mediatorInvitationUrl,
mediatorPickupStrategy: DidCommMediatorPickupStrategy.PickUpV2LiveMode,
},
mediator: false,
}),
},
})

// Connects to the mediator, requests mediation and starts message pickup
await alice.initialize()

DidCommMediatorPickupStrategy is exported from @credo-ts/didcomm. Pickup strategies explains the options.

During initialize(), Credo connects to the mediator if it isn't connected yet, requests mediation, waits for the grant and sets the mediator as the default. Then it starts picking up messages. Startup therefore depends on the mediator being reachable. To keep startup independent, leave out mediatorInvitationUrl and connect later with the API:

Method on agent.didcomm.mediationRecipientDescription
provision(connection)Request mediation over a connection, wait for the grant, and set the mediator as default.
initiateMessagePickup(mediator?, strategy?)Start picking up messages, from the default mediator unless you pass a mediation record.
stopMessagePickup()Stop all message pickup.
findDefaultMediator()Get the mediation record of the default mediator.
setDefaultMediator(mediationRecord)Change the default mediator.

Once a default mediator is set, new invitations and connections route through it without extra code:

wallet.ts
// The invitation points to the mediator's endpoint and routing keys
const outOfBandRecord = await alice.didcomm.oob.createInvitation({ label: 'Alice' })
const invitationUrl = outOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://alice.example.com' })

To use another mediator for one connection, create routing with agent.didcomm.mediationRecipient.getRouting({ mediatorId }) and pass it as routing to createInvitation() or receiveInvitation().

Pickup strategies​

mediatorPickupStrategy decides how the recipient gets its messages:

StrategyHow it works
PickUpV2LiveModeOpens a WebSocket, picks up queued messages, then asks the mediator to push new messages as they arrive (Pickup 2.0 live mode).
PickUpV2Polls with Pickup 2.0 every mediatorPollingInterval milliseconds.
PickUpV1Polls with Pickup 1.0 every mediatorPollingInterval milliseconds.
ImplicitOpens a WebSocket and keeps it open, so the mediator can send messages over it.
NoneDoesn't pick up messages automatically.

For a mobile wallet, use PickUpV2LiveMode: messages arrive as soon as the mediator receives them, and Credo reopens the WebSocket when it drops. It needs a WebSocket endpoint on the mediator and DidCommWsOutboundTransport on the wallet. Use PickUpV2 if the mediator has no WebSocket endpoint, and Implicit for mediators that don't support a pickup protocol.

Without a strategy, Credo asks the mediator which pickup protocols it supports and uses PickUpV2, PickUpV1 or Implicit, in that order. It never chooses live mode on its own. Credo stores the choice on the mediation record, and the stored choice wins over mediatorPickupStrategy from then on, so set the strategy before the agent first connects to the mediator.

OptionDefaultDescription
mediatorPollingInterval5000Milliseconds between polls for PickUpV1 and PickUpV2.
maximumMessagePickup10Messages per batch when polling.
baseMediatorReconnectionIntervalMs100First delay before reconnecting a closed WebSocket. It doubles after each attempt.
maximumMediatorReconnectionIntervalMsno limitLongest delay between reconnection attempts.

Control message pickup​

A mobile app usually stops pickup when it goes to the background and starts it again when it returns:

wallet.ts
// For example when the app goes to the background
await alice.didcomm.mediationRecipient.stopMessagePickup()

// Back in the foreground: picks up queued messages, then resumes live delivery
await alice.didcomm.mediationRecipient.initiateMessagePickup()

If you'd rather decide when pickup runs, set mediatorPickupStrategy: DidCommMediatorPickupStrategy.None and pass the strategy when you start it: initiateMessagePickup(undefined, DidCommMediatorPickupStrategy.PickUpV2LiveMode).

To fetch queued messages once, for example in a background task, use the message pickup API and wait until the messages are processed:

wallet.ts
const mediatorConnection = await alice.didcomm.mediationRecipient.findDefaultMediatorConnection()
if (mediatorConnection) {
await alice.didcomm.messagePickup.pickupMessages({
connectionId: mediatorConnection.id,
protocolVersion: 'v2',
awaitCompletion: true,
})
}

Keylist updates​

The mediator only forwards messages for keys in the recipient's keylist. Credo keeps the keylist up to date: every time it creates routing through a mediator, for a new invitation, connection or DID rotation, it adds the new key and waits for the mediator to confirm. Deleting a connection with connections.deleteById(), or with connections.hangup({ connectionId, deleteAfterHangup: true }), removes its keys again. After a DID rotation, connections.removePreviousDids({ connectionId }) removes the old keys.

Each confirmed update emits an event:

wallet.ts
alice.events.on<DidCommKeylistUpdatedEvent>(DidCommRoutingEventTypes.RecipientKeylistUpdated, ({ payload }) => {
for (const update of payload.keylist) {
console.log(`Mediator keylist update: ${update.action} ${update.recipientKey}`)
}
})

For keys created outside of Credo's routing, send the update yourself with agent.didcomm.mediationRecipient.notifyKeylistUpdate(connection, verkey, action).