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

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.
Fetches highlights and notes from Kavita with filtering options and spoiler protection.
YAML frontmatter, wikilinks to authors/series, and hierarchical tags from genres.
7 core services with dependency injection, tagged errors, and schema validation.
Semantic versioning, changelogs, stress testing, and 80% test coverage.
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
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],
}
) {}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),
);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.
Network failures with URL and status code
Authentication failures with reason
Schema validation failures with context
Vault file write failures with path
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}`;
}
}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
}) {}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.
10 test files using @effect/vitest with mock service layers.
Docker Compose with real Kavita instance and seeded test data.
Performance validation with 10,000+ annotations for v1.0.0.
Enforced thresholds: 80% lines, 70% functions, 60% branches.
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),
)
);
});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),
);Markdown Format
Annotations are formatted into beautifully structured markdown with YAML frontmatter, wikilinks, and hierarchical tags. Pure formatting functions ensure testability and consistent output.
Metadata, timestamps, and hierarchical tags for Obsidian queries
Auto-generated links to author and series notes
Multi-paragraph handling with page numbers
Genre and series tags in nested format
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-operaSide-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"));
}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.
Keep a Changelog format with semantic versioning
Obsidian plugin metadata and version info
Obsidian compatibility matrix per version
Development guide with git workflow
- 10,000+ annotation performance validation
- Production ready
- Chapter prefixes
- Multi-paragraph blockquotes
- Better tag defaults
- YAML frontmatter
- Wikilinks
- Hierarchical tags
- Genre extraction
- Core export functionality
- Basic annotation sync
Project Highlights
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.