🎯

services-layer

🎯Skill

from epicenterhq/epicenter

VibeIndex|
What it does

Defines service architecture patterns with domain-specific error handling, pure service design, and structured `Result` type management for robust, testable business logic.

πŸ“¦

Part of

epicenterhq/epicenter(28 items)

services-layer

Installation

git cloneClone repository
git clone https://github.com/EpicenterHQ/epicenter.git
BunRun with Bun
bun install # Will prompt to upgrade if your Bun version is too old
BunRun with Bun
bun dev
BunRun with Bun
bun install # Reinstall dependencies
BunRun with Bun
bun install
πŸ“– Extracted from docs: epicenterhq/epicenter
4Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Service layer patterns with createTaggedError, namespace exports, and Result types. Use when creating new services, defining domain-specific errors, or understanding the service architecture.

Overview

# Services Layer Patterns

This skill documents how to implement services in the Whispering architecture. Services are pure, isolated business logic with no UI dependencies that return Result types for error handling.

When to Apply This Skill

Use this pattern when you need to:

  • Create a new service with domain-specific error handling
  • Add error types with structured context (like HTTP status codes)
  • Understand how services are organized and exported
  • Implement platform-specific service variants (desktop vs web)

Core Architecture

Services follow a three-layer architecture: Service β†’ Query β†’ UI

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ UI β”‚ --> β”‚ RPC/Query β”‚ --> β”‚ Services β”‚

β”‚ Components β”‚ β”‚ Layer β”‚ β”‚ (Pure) β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

Services are:

  • Pure: Accept explicit parameters, no hidden dependencies
  • Isolated: No knowledge of UI state, settings, or reactive stores
  • Testable: Easy to unit test with mock parameters
  • Consistent: All return Result types for uniform error handling

Creating Tagged Errors with createTaggedError

Every service defines domain-specific errors using createTaggedError from wellcrafted:

```typescript

import { createTaggedError } from 'wellcrafted/error';

import { Err, Ok, type Result, tryAsync } from 'wellcrafted/result';

// Basic pattern - creates both constructor and Err helper

export const { MyServiceError, MyServiceErr } =

createTaggedError('MyServiceError');

type MyServiceError = ReturnType;

```

What createTaggedError Returns

createTaggedError('Name') returns an object with two properties:

  1. NameError - Constructor function for creating error objects
  2. NameErr - Helper that wraps the error in Err() for direct return

```typescript

// These are equivalent:

return Err(MyServiceError({ message: 'Something failed' }));

return MyServiceErr({ message: 'Something failed' }); // Shorter form

```

Adding Typed Context with .withContext()

For errors that need structured metadata (like HTTP status codes), chain .withContext():

```typescript

type ResponseContext = {

status: number; // HTTP status code

};

export const { ResponseError, ResponseErr } =

createTaggedError('ResponseError').withContext();

// Usage: Include context when creating errors

return ResponseErr({

message: 'Request failed',

context: { status: 401 }, // TypeScript enforces this shape

});

```

Error Type Examples from the Codebase

```typescript

// Simple service error (most common)

export const { RecorderServiceError, RecorderServiceErr } = createTaggedError(

'RecorderServiceError',

);

// HTTP errors with status context

export const { ResponseError, ResponseErr } = createTaggedError(

'ResponseError',

).withContext<{ status: number }>();

// Multiple related errors

export const { ConnectionError, ConnectionErr } =

createTaggedError('ConnectionError');

export const { ParseError, ParseErr } = createTaggedError('ParseError');

// Combine into union type

export type HttpServiceError = ConnectionError | ResponseError | ParseError;

```

Service Implementation Pattern

Basic Service Structure

```typescript

import { createTaggedError, extractErrorMessage } from 'wellcrafted/error';

import { Err, Ok, type Result, tryAsync, trySync } from 'wellcrafted/result';

// 1. Define domain-specific error type

export const { MyServiceError, MyServiceErr } =

createTaggedError('MyServiceError');

type MyServiceError = ReturnType;

// 2. Create factory function that returns service object

export function createMyService() {

return {

async doSomething(options: {

param1: string;

param2: number;

}): Promise> {

// Input validation

if (!options.param1) {

return MyServiceErr({

message: 'param1 is required',

});

}

// Wrap risky operations with tryAsync

const { data, error } = await tryAsync({

try: () => riskyAsyncOperation(options),

catch: (error) =>

MyServiceErr({

message: Operation failed: ${extractErrorMessage(error)},

}),

});

if (error) return Err(error);

return Ok(data);

},

};

}

// 3. Export the "Live" instance (production singleton)

export type MyService = ReturnType;

export const MyServiceLive = createMyService();

```

Real-World Example: Recorder Service

```typescript

// From apps/whispering/src/lib/services/isomorphic/recorder/navigator.ts

export function createNavigatorRecorderService(): RecorderService {

let activeRecording: ActiveRecording | null = null;

return {

getRecorderState: async (): Promise<

Result

> => {

return Ok(activeRecording ? 'RECORDING' : 'IDLE');

},

startRecording: async (

params: NavigatorRecordingParams,

{ sendStatus },

): Promise> => {

// Validate state

if (activeRecording) {

return RecorderServiceErr({

message:

'A recording is already in progress. Please stop the current recording.',

});

}

// Get stream (calls another service)

const { data: streamResult, error: acquireStreamError } =

await getRecordingStream({ selectedDeviceId, sendStatus });

if (acquireStreamError) {

return RecorderServiceErr({

message: acquireStreamError.message,

});

}

// Initialize MediaRecorder

const { data: mediaRecorder, error: recorderError } = trySync({

try: () =>

new MediaRecorder(stream, {

bitsPerSecond: Number(bitrateKbps) * 1000,

}),

catch: (error) =>

RecorderServiceErr({

message: Failed to initialize recorder. ${extractErrorMessage(error)},

}),

});

if (recorderError) {

cleanupRecordingStream(stream);

return Err(recorderError);

}

// Store state and start

activeRecording = {

recordingId,

stream,

mediaRecorder,

recordedChunks: [],

};

mediaRecorder.start(TIMESLICE_MS);

return Ok(deviceOutcome);

},

};

}

export const NavigatorRecorderServiceLive = createNavigatorRecorderService();

```

Namespace Exports Pattern

Services are organized hierarchically and re-exported as namespace objects:

Folder Structure

```

services/

β”œβ”€β”€ desktop/ # Desktop-only (Tauri)

β”‚ β”œβ”€β”€ index.ts # Re-exports as desktopServices

β”‚ β”œβ”€β”€ command.ts

β”‚ └── ffmpeg.ts

β”œβ”€β”€ isomorphic/ # Cross-platform

β”‚ β”œβ”€β”€ index.ts # Re-exports as services

β”‚ β”œβ”€β”€ transcription/

β”‚ β”‚ β”œβ”€β”€ index.ts # Re-exports as transcriptions namespace

β”‚ β”‚ β”œβ”€β”€ cloud/

β”‚ β”‚ β”‚ β”œβ”€β”€ openai.ts

β”‚ β”‚ β”‚ └── groq.ts

β”‚ β”‚ └── local/

β”‚ β”‚ └── whispercpp.ts

β”‚ └── completion/

β”‚ β”œβ”€β”€ index.ts

β”‚ └── openai.ts

β”œβ”€β”€ types.ts

└── index.ts # Main entry point

```

Index File Pattern

```typescript

// services/isomorphic/transcription/index.ts

export { OpenaiTranscriptionServiceLive as openai } from './cloud/openai';

export { GroqTranscriptionServiceLive as groq } from './cloud/groq';

export { WhispercppTranscriptionServiceLive as whispercpp } from './local/whispercpp';

// services/isomorphic/index.ts

import * as transcriptions from './transcription';

import * as completions from './completion';

export const services = {

db: DbServiceLive,

sound: PlaySoundServiceLive,

transcriptions, // Namespace import

completions, // Namespace import

} as const;

// services/index.ts (main entry)

export { services } from './isomorphic';

export { desktopServices } from './desktop';

```

Consuming Services

```typescript

// In query layer or anywhere

import { services, desktopServices } from '$lib/services';

// Access via namespace

await services.transcriptions.openai.transcribe(blob, options);

await services.transcriptions.groq.transcribe(blob, options);

await services.db.recordings.getAll();

await desktopServices.ffmpeg.compressAudioBlob(blob, options);

```

Platform-Specific Services

For services that need different implementations per platform:

Define Shared Interface

```typescript

// services/isomorphic/text/types.ts

export type TextService = {

readFromClipboard(): Promise>;

copyToClipboard(text: string): Promise>;

writeToCursor(text: string): Promise>;

};

```

Implement Per Platform

```typescript

// services/isomorphic/text/desktop.ts

export function createTextServiceDesktop(): TextService {

return {

copyToClipboard: (text) =>

tryAsync({

try: () => writeText(text), // Tauri API

catch: (error) => TextServiceErr({ message: 'Clipboard write failed' }),

}),

};

}

// services/isomorphic/text/web.ts

export function createTextServiceWeb(): TextService {

return {

copyToClipboard: (text) =>

tryAsync({

try: () => navigator.clipboard.writeText(text), // Browser API

catch: (error) => TextServiceErr({ message: 'Clipboard write failed' }),

}),

};

}

```

Build-Time Platform Detection

```typescript

// services/isomorphic/text/index.ts

export const TextServiceLive = window.__TAURI_INTERNALS__

? createTextServiceDesktop()

: createTextServiceWeb();

```

Error Message Best Practices

Write error messages that are:

  • User-friendly: Explain what happened in plain language
  • Actionable: Suggest what the user can do
  • Detailed: Include technical details for debugging

```typescript

// Good error messages

return RecorderServiceErr({

message:

'Unable to connect to the selected microphone. This could be because the device is already in use by another application, has been disconnected, or lacks proper permissions.',

});

return MyServiceErr({

message: Failed to parse configuration file. Please check that ${filename} contains valid JSON.,

});

// Include technical details with extractErrorMessage

return MyServiceErr({

message: Database operation failed. ${extractErrorMessage(error)},

});

```

Key Rules

  1. Services never import settings - Pass configuration as parameters
  2. Services never import UI code - No toasts, no notifications, no WhisperingError
  3. Always return Result types - Never throw errors
  4. Use trySync/tryAsync - See the error-handling skill for details
  5. Export factory + Live instance - Factory for testing, Live for production
  6. Name errors consistently - {ServiceName}ServiceError pattern

References

  • See apps/whispering/src/lib/services/README.md for architecture details
  • See the query-layer skill for how services are consumed
  • See the error-handling skill for trySync/tryAsync patterns