Back to Portfolio
Open SourceTechnologyDec 8, 2025

Kavita to Obsidian

Event
The Innovation Lab
Overview

An Obsidian plugin that syncs reading annotations from Kavita into your vault as searchable markdown. Built with Effect-TS for type-safe error handling, comprehensive testing with 80% coverage, and professional OSS practices including semantic versioning, changelogs, and stress testing for 10,000+ annotations.

Technologies
Effect-TSTypeScriptObsidianOpen SourceTestingPlugin Development
Kavita to Obsidian preview
Open Source

Overview

Kavita to Obsidian is an Obsidian plugin that syncs your reading annotations, highlights, and notes from Kavita into your vault as beautifully formatted, searchable markdown files.

Built with Effect-TS for type-safe error handling and dependency injection, this project demonstrates professional patterns for plugin development, comprehensive testing strategies, and proper open source management practices.

Annotation Sync

Fetches highlights and notes from Kavita with filtering options and spoiler protection.

Rich Markdown

YAML frontmatter, wikilinks to authors/series, and hierarchical tags from genres.

Effect Services

7 core services with dependency injection, tagged errors, and schema validation.

OSS Ready

Semantic versioning, changelogs, stress testing, and 80% test coverage.

System Design

Effect Services Architecture

The plugin uses Effect-TS's service pattern for clean dependency injection and testability. Each service encapsulates a specific concern and can be provided with mock implementations during testing.

PluginConfig

Configuration from settings or environment

KavitaClient

11 API methods with JWT auth and schema validation

ObsidianAdapter

Vault file operations (read, write, list)

AnnotationSyncer

Orchestrates multi-step sync workflow

Service Pattern

Each service defines its dependencies and implementation using Effect.Service

// Effect Service pattern example
export class KavitaClient extends Effect.Service<KavitaClient>()(
  "KavitaClient",
  {
    effect: Effect.gen(function* () {
      const config = yield* PluginConfig;
      const httpClient = yield* HttpClient.HttpClient;

      return {
        fetchAllAnnotations: () =>
          Effect.gen(function* () {
            const response = yield* httpClient.get("/api/Annotation");
            return yield* HttpClientResponse.schemaBodyJson(
              Schema.Array(AnnotationDto)
            )(response);
          }).pipe(
            Effect.mapError((e) => new KavitaNetworkError({ url: "/api/Annotation", cause: e }))
          ),
      };
    }),
    dependencies: [PluginConfig.Default],
  }
) {}
Effect.ServiceDependency InjectionSchema Validation
Layer Composition

Services are composed into layers that can be swapped for testing

// Layer composition in plugin entry
const ConfigLayer = PluginConfig.fromSettings(this.settings);
const ObsidianAppLayer = Layer.succeed(ObsidianApp, this.app);

const ObsidianAdapterLayer = ObsidianAdapter.Default.pipe(
  Layer.provide(ObsidianAppLayer),
);

const KavitaClientLayer = KavitaClient.DefaultWithoutDependencies.pipe(
  Layer.provide(ConfigLayer),
  Layer.provide(ObsidianHttpClient),
);

const SyncerLayer = AnnotationSyncer.Default.pipe(
  Layer.provide(KavitaClientLayer),
  Layer.provide(ObsidianAdapterLayer),
  Layer.provide(ConfigLayer),
);
Layer.provideTestabilityPlugin Lifecycle
Type Safety

Error Handling & Schemas

All errors extend Schema.TaggedError with structured properties, enabling exhaustive error handling at compile time. API responses are validated against 25+ DTO schemas, catching data inconsistencies before they cause runtime failures.

KavitaNetworkError

Network failures with URL and status code

KavitaAuthError

Authentication failures with reason

KavitaParseError

Schema validation failures with context

ObsidianWriteError

Vault file write failures with path

Tagged Errors

Structured errors with compile-time exhaustive handling

// Tagged errors with structured properties
export class KavitaNetworkError extends Schema.TaggedError<KavitaNetworkError>()(
  "KavitaNetworkError",
  {
    url: Schema.String,
    statusCode: Schema.optionalWith(Schema.Number, { exact: true }),
    cause: Schema.optionalWith(Schema.Defect, { exact: true }),
  }
) {
  override get message(): string {
    return `Network error calling ${this.url}`;
  }
}

export class KavitaAuthError extends Schema.TaggedError<KavitaAuthError>()(
  "KavitaAuthError",
  {
    reason: Schema.String,
  }
) {
  override get message(): string {
    return `Authentication failed: ${this.reason}`;
  }
}
Schema.TaggedErrorExhaustive MatchingStructured Data
Schema Validation

25+ DTO classes for compile-time API response guarantees

// 25+ DTO schemas for API responses
export class AnnotationDto extends Schema.Class<AnnotationDto>("AnnotationDto")({
  id: Schema.Number,
  content: Schema.String,
  pageNumber: Schema.Number,
  chapterId: Schema.Number,
  seriesId: Schema.Number,
  libraryId: Schema.Number,
  isSpoiler: Schema.Boolean,
  createdUtc: Schema.String,
  lastModifiedUtc: Schema.String,
  // ... 8 more fields
}) {}

export class SeriesMetadataDto extends Schema.Class<SeriesMetadataDto>(
  "SeriesMetadataDto"
)({
  writers: Schema.Array(PersonDto),
  genres: Schema.Array(GenreDto),
  tags: Schema.Array(TagDto),
  // Complex nested structure with full validation
}) {}
Schema.ClassAutomatic DecodingRuntime Validation
Quality Assurance

Comprehensive Testing

The project employs a multi-layered testing strategy: unit tests with Effect-aware assertions, integration tests against a real Kavita instance via Docker, and stress tests validating performance with 10,000+ annotations.

Unit Tests

10 test files using @effect/vitest with mock service layers.

Integration Tests

Docker Compose with real Kavita instance and seeded test data.

Stress Testing

Performance validation with 10,000+ annotations for v1.0.0.

Coverage

Enforced thresholds: 80% lines, 70% functions, 60% branches.

Coverage Thresholds
80%
Lines
80%
Statements
70%
Functions
60%
Branches
Unit Testing Pattern

Effect-aware test assertions with mock service layers

import { describe, it } from "@effect/vitest";
import { Effect, Layer } from "effect";

describe("AnnotationSyncer", () => {
  it.effect("syncs annotations to vault", () =>
    Effect.gen(function* () {
      const syncer = yield* AnnotationSyncer;
      const result = yield* syncer.syncAnnotations();

      expect(result.annotationCount).toBeGreaterThan(0);
      expect(result.filePath).toContain(".md");
    }).pipe(
      Effect.provide(AnnotationSyncer.Default),
      Effect.provide(MockKavitaClient),
      Effect.provide(MockObsidianAdapter),
    )
  );
});
@effect/vitestMock LayersType-Safe
Integration Testing

Tests against real Kavita instance via Docker

// Docker-based integration test
const program = Effect.gen(function* () {
  const client = yield* KavitaClient;

  // Test against real Kavita instance
  const annotations = yield* client.fetchAllAnnotations();
  const libraries = yield* client.getLibraries();

  expect(annotations.length).toBeGreaterThan(0);
  expect(libraries.length).toBeGreaterThan(0);
}).pipe(
  Effect.provide(KavitaClient.Default),
  Effect.provide(PluginConfig.fromEnv),
);
DockerReal APIE2E
Generated Output

Markdown Format

Annotations are formatted into beautifully structured markdown with YAML frontmatter, wikilinks, and hierarchical tags. Pure formatting functions ensure testability and consistent output.

YAML Frontmatter

Metadata, timestamps, and hierarchical tags for Obsidian queries

Wikilinks

Auto-generated links to author and series notes

Smart Blockquotes

Multi-paragraph handling with page numbers

Hierarchical Tags

Genre and series tags in nested format

Example Output

Generated markdown with frontmatter, blockquotes, and links

---
title: Reading Annotations
synced_at: 2025-12-08T10:30:00.000Z
source: kavita
tags:
  - fiction/sci-fi
  - series/the-expanse
  - book/leviathan-wakes
---

## The Expanse

### Leviathan Wakes

#### Chapter 1: Prologue

> "The stars are better off without us."
>
> — Page 12

*A haunting opening that sets the tone for the entire series.*

> "There was a button. Miller pushed it."
>
> — Page 45

#### Chapter 2: Miller

> "Doors and corners, kid. That's where they get you."
>
> — Page 78

[[James S.A. Corey]] | #fiction #sci-fi #space-opera
YAMLWikilinksTagsBlockquotes
Pure Formatters

Side-effect free formatting functions for testability

// Pure formatting functions
export function formatAnnotation(
  annotation: AnnotationDto,
  config: PluginConfigShape
): Option.Option<string> {
  // Filter spoilers if configured
  if (annotation.isSpoiler && !config.includeSpoilers) {
    return Option.none();
  }

  const lines: string[] = [];

  // Blockquote with proper multi-line handling
  const paragraphs = annotation.content.split("\n\n");
  lines.push("> " + paragraphs.join("\n>\n> "));

  // Page number if available
  if (annotation.pageNumber > 0) {
    lines.push(`>\n> — Page ${annotation.pageNumber}`);
  }

  // User comment if present
  if (annotation.comment) {
    lines.push(`\n*${annotation.comment}*`);
  }

  return Option.some(lines.join("\n"));
}
Option<T>Pure FunctionsTestable
Open Source

Release Management

Professional open source practices with semantic versioning, structured changelogs, and clear contribution guidelines. The project ships with all standard OSS artifacts for Obsidian plugin distribution.

CHANGELOG.md

Keep a Changelog format with semantic versioning

manifest.json

Obsidian plugin metadata and version info

versions.json

Obsidian compatibility matrix per version

CONTRIBUTING.md

Development guide with git workflow

Release History
v1.0.0Stress Testing Support
  • 10,000+ annotation performance validation
  • Production ready
2025-12-08
v0.0.3Improved Display
  • Chapter prefixes
  • Multi-paragraph blockquotes
  • Better tag defaults
2025-12-07
v0.0.2Enriched Markdown
  • YAML frontmatter
  • Wikilinks
  • Hierarchical tags
  • Genre extraction
2025-12-06
v0.0.1Initial Release
  • Core export functionality
  • Basic annotation sync
2025-12-05
Summary

Project Highlights

4,443
TypeScript Lines
7
Effect Services
80%
Test Coverage
11
API Endpoints

Kavita to Obsidian demonstrates professional patterns for building maintainable, testable software with Effect-TS. From type-safe error handling to comprehensive testing strategies, this project serves as a reference implementation for modern TypeScript development.

Ready to transform your engineering?

Whether you need technical leadership, enterprise development, or team optimization—let's discuss how we can help.