Skip to main content

Services overview

Chive's backend is organized into 18 specialized services that handle distinct responsibilities. All services follow ATProto 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 PreprintService {
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()
PreprintServicePreprint indexingindexPreprint(), getPreprint()
ReviewServiceReview/endorsement indexingindexReview(), getReviews()

Search and discovery

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

User engagement

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

Author and identity

ServicePurposeKey operations
ClaimingServiceAuthorship verificationstartClaim(), collectEvidence()
ReconciliationServiceImport reconciliationcreateReconciliation(), updateStatus()

Infrastructure

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

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 (Preprint, 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 getPreprint(uri: AtUri): Promise<Result<PreprintView, PreprintError>> {
const record = await this.storage.getPreprint(uri);
if (!record) {
return err(new PreprintNotFoundError(uri));
}
return ok(this.toView(record));
}

Callers handle both success and failure:

const result = await preprintService.getPreprint(uri);
result.match(
(preprint) => console.log(preprint.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('PreprintService', () => {
let service: PreprintService;
let mockStorage: MockStorageBackend;

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

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

Next steps