Skip to main content

Services overview

Chive's backend is organized into 18 specialized services that handle distinct responsibilities. All services follow AT Protocol compliance principles: they index data from the firehose but never write to user PDSes.

Service architecture

Services use TSyringe for dependency injection with abstract I* interfaces:

import { injectable, inject } from 'tsyringe';

@injectable()
export class EprintService {
constructor(
@inject('IStorageBackend') private storage: IStorageBackend,
@inject('ISearchEngine') private search: ISearchEngine,
@inject('ILogger') private logger: ILogger
) {}
}

All services return Result<T, Error> types for explicit error handling.

Core services

Data ingestion

ServicePurposeKey operations
IndexingServiceFirehose consumptionstart(), stop(), getStatus()
EprintServiceEprint indexingindexEprint(), getEprint()
ReviewServiceReview/endorsement indexingindexReview(), getReviews()
CollectionServiceCollection indexinggetCollection(), findContainsEdge()

Search and discovery

ServicePurposeKey operations
SearchServiceFull-text searchsearch(), autocomplete()
DiscoveryServiceRecommendationsgetRecommendationsForUser(), enrichEprint()
KnowledgeGraphServiceField taxonomygetField(), browseFaceted()

User engagement

ServicePurposeKey operations
MetricsServiceView/download trackingrecordView(), getTrending()
ActivityServiceActivity logginglogActivity(), correlateWithFirehose()
NotificationServiceReal-time notificationscreateNotification(), getUnreadCount()

Author and identity

ServicePurposeKey operations
ClaimingServicePaper claiminggetSubmissionData(), requestCoauthorship()
ReconciliationServiceImport reconciliationcreateReconciliation(), updateStatus()

Infrastructure

ServicePurposeKey operations
BlobProxyServiceBlob fetchinggetBlob(), proxyBlob()
BacklinkServiceATProto backlinkscreateBacklink(), getCounts()
GovernancePDSConnectorGovernance PDS accessgetAuthorityRecord(), listFacets()
SchemaCompatibilityServiceSchema evolutiondetectAbstractFormat(), needsMigration()

Service initialization order

Services are initialized in dependency order:

1. Storage adapters (PostgreSQL, Redis, Elasticsearch, Neo4j)
2. Infrastructure services (BlobProxy, GovernancePDS)
3. Core indexing services (Eprint, Review)
4. Query services (Search, Discovery, KnowledgeGraph)
5. Engagement services (Metrics, Activity, Notification)
6. Application services (Claiming, Reconciliation)
7. IndexingService (starts firehose consumption)

Error handling patterns

Services use the Result type for operations that can fail:

import { Result, ok, err } from 'neverthrow';

async getEprint(uri: AtUri): Promise<Result<EprintView, EprintError>> {
const record = await this.storage.getEprint(uri);
if (!record) {
return err(new EprintNotFoundError(uri));
}
return ok(this.toView(record));
}

Callers handle both success and failure:

const result = await eprintService.getEprint(uri);
result.match(
(eprint) => console.log(eprint.title),
(error) => console.error(error.message)
);

Caching strategies

Services use a 3-tier caching strategy:

TierStorageTTLUse case
L1Redis5 minHot data, session state
L2PostgreSQL24 hoursIndexed metadata
L3PDS fetchOn demandAuthoritative source

The BlobProxyService demonstrates this pattern:

async getBlob(did: string, cid: CID): Promise<Blob> {
// L1: Check Redis
const cached = await this.redis.get(`blob:${cid}`);
if (cached) return cached;

// L2: Check CDN
const cdnUrl = await this.cdn.getUrl(cid);
if (cdnUrl) return this.fetchFromCdn(cdnUrl);

// L3: Fetch from user's PDS
const blob = await this.repository.getBlob(did, cid);
await this.redis.set(`blob:${cid}`, blob, 'EX', 300);
return blob;
}

Health checks

Each service exposes health status:

interface ServiceHealth {
status: 'healthy' | 'degraded' | 'unhealthy';
lastCheck: Date;
details?: Record<string, unknown>;
}

// Usage
const health = await indexingService.getStatus();
// { status: 'healthy', cursor: 12345678, lag: '2s' }

Testing services

Services are tested at multiple levels:

// Unit test with mocked dependencies
describe('EprintService', () => {
let service: EprintService;
let mockStorage: MockStorageBackend;

beforeEach(() => {
mockStorage = new MockStorageBackend();
service = new EprintService(mockStorage, mockSearch, mockLogger);
});

it('indexes eprint from firehose event', async () => {
const result = await service.indexEprint(record, metadata);
expect(result.isOk()).toBe(true);
expect(mockStorage.store).toHaveBeenCalledWith(
expect.objectContaining({
uri: record.uri,
})
);
});
});

Next steps