Skip to main content

Services Layer Overview

The services layer (lib/services/) contains 36+ service modules that implement the application's core business logic. Services sit between API route handlers and the data access layer (repositories and database queries), following the pattern: Route Handler -> Service -> Repository/Query.

Service Inventory

Content Management Services

ServiceFileDescription
ItemGitServiceitem-git.service.tsGit-based item CRUD with YAML file operations
CategoryGitServicecategory-git.service.tsGit-based category management
CollectionGitServicecollection-git.service.tsGit-based collection management
TagGitServicetag-git.service.tsGit-based tag management
CategoryFileServicecategory-file.service.tsFile-based category operations
FileServicefile.service.tsGeneric YAML file read/write service
SyncManagersync-service.tsBackground content repository synchronization

User & Role Services

ServiceFileDescription
RoleDbServicerole-db.service.tsRole CRUD with permission management
UserDbServiceuser-db.service.tsUser account management

Engagement & Moderation Services

ServiceFileDescription
engagement.serviceengagement.service.tsPopularity scoring and engagement metrics
moderation.servicemoderation.service.tsContent moderation actions and workflows
SurveyServicesurvey.service.tsSurvey CRUD and response management
ItemAuditServiceitem-audit.service.tsItem change audit trail

Payment & Subscription Services

ServiceFileDescription
StripeProductsServicestripe-products.service.tsStripe product/price synchronization
SubscriptionServicesubscription.service.tsProvider-agnostic subscription management
WebhookSubscriptionServicewebhook-subscription.service.tsWebhook event processing for subscriptions
subscription-jobssubscription-jobs.tsSubscription background job scheduling

Notification Services

ServiceFileDescription
EmailNotificationServiceemail-notification.service.tsTransactional email sending
NotificationServicenotification.service.tsIn-app notification management

Analytics Services

ServiceFileDescription
PostHogApiServiceposthog-api.service.tsPostHog analytics API integration
AnalyticsBackgroundProcessoranalytics-background-processor.tsBackground analytics processing
AnalyticsExportServiceanalytics-export.service.tsAnalytics data export
AnalyticsScheduledReportsanalytics-scheduled-reports.service.tsScheduled analytics report generation

Integration Services

ServiceFileDescription
TwentyCrmApiServicetwenty-crm-api.service.tsTwenty CRM connection testing
TwentyCrmRestClienttwenty-crm-rest-client.service.tsTwenty CRM REST client with retry logic
TwentyCrmSyncServicetwenty-crm-sync.service.tsBidirectional CRM data synchronization
TwentyCrmSyncFactorytwenty-crm-sync-factory.tsCRM sync instance factory
TwentyCrmConfigDbServicetwenty-crm-config-db.service.tsCRM configuration persistence

Location & Geocoding Services

ServiceFileDescription
geocoding/*geocoding/Address geocoding services
location/*location/Location data services (countries, cities, coordinates)

Utility Services

ServiceFileDescription
CurrencyServicecurrency.service.tsCurrency conversion and detection
CurrencyDetectionServicecurrency-detection.service.tsAuto-detect user currency from location
CompanyServicecompany.service.tsCompany profile management
SettingsServicesettings.service.tsApplication settings management
SponsorAdServicesponsor-ad.service.tsSponsor advertisement management

Architecture Patterns

Service Instantiation

Services use two instantiation patterns:

Class-based (most services):

export class ItemGitService {
private config: ItemGitServiceConfig;

constructor(config: ItemGitServiceConfig) {
this.config = config;
}

async initialize(): Promise<void> { ... }
async create(data: CreateItemRequest): Promise<ItemData> { ... }
async update(id: string, data: UpdateItemRequest): Promise<ItemData> { ... }
}

Singleton (shared services):

// SyncManager - singleton with module-level state
class SyncManager {
private syncInProgress = false;
private lastSyncTime: Date | null = null;

async performSync(): Promise<SyncResult> { ... }
}

Dependency Pattern

Services depend on repositories and database query modules, not on each other directly:

API Route Handler
-> Service (business logic, validation, orchestration)
-> Repository (data access abstraction)
-> Database Queries (Drizzle ORM queries)

For example, the moderation service depends on:

  • moderation.queries for database operations
  • comment.queries for comment lookups
  • ItemRepository for item operations
  • EmailNotificationService for sending notifications

Error Handling

Services return typed result objects rather than throwing exceptions:

interface ModerationResult {
success: boolean;
message: string;
error?: string;
}

interface SyncResult {
success: boolean;
message: string;
details?: string;
duration?: number;
}

This pattern allows callers to handle errors without try/catch boilerplate and provides consistent error reporting.

Server-Only Enforcement

Several services use the server-only import to prevent accidental client-side bundling:

import 'server-only';

This triggers a build error if the module is imported in a client component, ensuring database connections and API keys are never exposed to the browser.

Service Configuration

Services receive configuration through:

  1. Constructor injection: Services like ItemGitService accept a config object
  2. ConfigService: Services like PostHogApiService use the centralized lib/config/config-service.ts
  3. Environment variables: Some services read env vars directly for simple configuration

Common Configuration Types

// Git service configuration
interface ItemGitServiceConfig {
owner: string; // GitHub owner
repo: string; // Repository name
token: string; // GitHub token
branch: string; // Git branch
dataDir: string; // Local data directory
itemsDir: string; // Items subdirectory
}

// Sync configuration
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const SYNC_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const MAX_RETRIES = 3;

Module Exports

The main lib/services/index.ts exports core shared services:

export {
FileService,
createFileService,
fileServices,
type YamlData,
type FileServiceConfig,
} from './file.service';

Most services are imported directly from their individual files rather than through the barrel export, as they have specific initialization requirements.

Testing Services

Since services encapsulate business logic separate from HTTP concerns, they are the natural boundary for unit testing:

// Example test structure
const service = new RoleDbService();
const role = await service.create({
name: 'Test Role',
permissions: ['items:read', 'items:create'],
});
expect(role.name).toBe('Test Role');

Services that depend on external APIs (PostHog, Twenty CRM) should be tested with mocked HTTP clients.