Skip to main content

Overview

Credo provides a flexible storage layer with two main components:
  • StorageService: Low-level storage interface
  • Repository: High-level data access with event emission

StorageService

The StorageService interface defines the contract for storage implementations.

Interface

interface StorageService<T extends BaseRecord> {
  save(agentContext: AgentContext, record: T): Promise<void>
  update(agentContext: AgentContext, record: T): Promise<void>
  delete(agentContext: AgentContext, record: T): Promise<void>
  deleteById(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, id: string): Promise<void>
  getById(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, id: string): Promise<T>
  getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>): Promise<T[]>
  findByQuery(
    agentContext: AgentContext,
    recordClass: BaseRecordConstructor<T>,
    query: Query<T>,
    queryOptions?: QueryOptions
  ): Promise<T[]>
}

Query Options

type QueryOptions = {
  limit?: number
  offset?: number
  cursor?: {
    before?: string
    after?: string
  }
}

Query Syntax

// Simple query
const query: Query<DidCommConnectionRecord> = {
  state: DidCommConnectionState.Completed,
  theirLabel: 'Alice',
}

// Advanced query with operators
const advancedQuery: Query<DidCommConnectionRecord> = {
  $or: [
    { state: DidCommConnectionState.Completed },
    { state: DidCommConnectionState.ResponseSent },
  ],
  $and: [
    { role: DidCommConnectionRole.Inviter },
  ],
}

Repository

The Repository class provides high-level access to stored records with automatic event emission.

Constructor

import { Repository } from '@credo-ts/core'

const repository = new Repository(
  recordClass,
  storageService,
  eventEmitter
)

Methods

save()

Save a new record.
await repository.save(agentContext, record)
agentContext
AgentContext
required
Agent context
record
T
required
Record to save
Returns: Promise<void> Throws: RecordDuplicateError if record with ID already exists Emits: RecordSavedEvent

update()

Update an existing record.
await repository.update(agentContext, record)
agentContext
AgentContext
required
Agent context
record
T
required
Record to update
Returns: Promise<void> Throws: RecordNotFoundError if record doesn’t exist Emits: RecordUpdatedEvent

updateByIdWithLock()

Update a record with locking support (if storage supports it).
const updatedRecord = await repository.updateByIdWithLock(
  agentContext,
  recordId,
  async (record) => {
    record.metadata.set('counter', record.metadata.get('counter') + 1)
    return record
  }
)
agentContext
AgentContext
required
Agent context
id
string
required
Record ID
updateCallback
(record: T) => Promise<T>
required
Callback that receives the record and returns the updated record
Returns: Promise<T> Emits: RecordUpdatedEvent

delete()

Delete a record.
await repository.delete(agentContext, record)
agentContext
AgentContext
required
Agent context
record
T
required
Record to delete
Returns: Promise<void> Emits: RecordDeletedEvent

deleteById()

Delete a record by ID.
await repository.deleteById(agentContext, recordId)
agentContext
AgentContext
required
Agent context
id
string
required
Record ID
Returns: Promise<void> Throws: RecordNotFoundError if record doesn’t exist Emits: RecordDeletedEvent

getById()

Retrieve a record by ID.
const record = await repository.getById(agentContext, recordId)
agentContext
AgentContext
required
Agent context
id
string
required
Record ID
Returns: Promise<T> Throws: RecordNotFoundError if record doesn’t exist

findById()

Find a record by ID (returns null if not found).
const record = await repository.findById(agentContext, recordId)

if (record) {
  console.log('Found record')
} else {
  console.log('Record not found')
}
agentContext
AgentContext
required
Agent context
id
string
required
Record ID
Returns: Promise<T | null>

getAll()

Retrieve all records.
const records = await repository.getAll(agentContext)
agentContext
AgentContext
required
Agent context
Returns: Promise<T[]>

findByQuery()

Find records by query.
const records = await repository.findByQuery(
  agentContext,
  {
    state: DidCommConnectionState.Completed,
  },
  {
    limit: 10,
    offset: 0,
  }
)
agentContext
AgentContext
required
Agent context
query
Query<T>
required
Query object
queryOptions
QueryOptions
Query options (limit, offset, cursor)
Returns: Promise<T[]>

findSingleByQuery()

Find a single record by query.
const record = await repository.findSingleByQuery(
  agentContext,
  { did: 'did:key:...' }
)

if (record) {
  console.log('Found record')
}
agentContext
AgentContext
required
Agent context
query
Query<T>
required
Query object
options
{ cacheKey?: string }
Options including optional cache key
Returns: Promise<T | null> Throws: RecordDuplicateError if multiple records match

getSingleByQuery()

Get a single record by query (throws if not found).
const record = await repository.getSingleByQuery(
  agentContext,
  { did: 'did:key:...' }
)
agentContext
AgentContext
required
Agent context
query
Query<T>
required
Query object
Returns: Promise<T> Throws: RecordNotFoundError if no record matches, RecordDuplicateError if multiple records match

supportsLocking()

Check if the storage service supports locking.
if (repository.supportsLocking(agentContext)) {
  // Use updateByIdWithLock for concurrent updates
} else {
  // Use regular update
}
agentContext
AgentContext
required
Agent context
Returns: boolean

Creating Custom Repositories

import { BaseRecord, Repository, injectable, inject } from '@credo-ts/core'

class MyCustomRecord extends BaseRecord {
  public static readonly type = 'MyCustomRecord'
  
  public name!: string
  public value!: string
  
  public getTags() {
    return {
      ...this._tags,
      name: this.name,
    }
  }
}

@injectable()
class MyCustomRepository extends Repository<MyCustomRecord> {
  public constructor(
    @inject(InjectionSymbols.StorageService) storageService: StorageService<MyCustomRecord>,
    eventEmitter: EventEmitter
  ) {
    super(MyCustomRecord, storageService, eventEmitter)
  }
  
  // Add custom methods
  public async findByName(agentContext: AgentContext, name: string) {
    return this.findSingleByQuery(agentContext, { name })
  }
}

Storage Events

import { RepositoryEventTypes } from '@credo-ts/core'

agent.events.on(RepositoryEventTypes.RecordSaved, (event) => {
  console.log('Record saved:', event.payload.record)
})

agent.events.on(RepositoryEventTypes.RecordUpdated, (event) => {
  console.log('Record updated:', event.payload.record)
})

agent.events.on(RepositoryEventTypes.RecordDeleted, (event) => {
  console.log('Record deleted:', event.payload.record)
})

Storage Implementations

Credo supports multiple storage backends:
import { AskarModule } from '@credo-ts/askar'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'

const agent = new Agent({
  // ...
  modules: {
    askar: new AskarModule({ ariesAskar }),
  },
})

Drizzle

import { DrizzleStorageModule } from '@credo-ts/drizzle-storage'

const agent = new Agent({
  // ...
  modules: {
    drizzleStorage: new DrizzleStorageModule({
      // Database configuration
    }),
  },
})

See Also

Build docs developers (and LLMs) love