Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rivenmedia/riven-ts/llms.txt

Use this file to discover all available pages before exploring further.

The fastest path from zero to a working plugin is the Turborepo generator built into the monorepo. It creates the full package structure, registers the plugin as a workspace dependency in apps/riven, and auto-formats the output — so you start with valid, compiling code and only need to fill in your integration logic.

Prerequisites

Before you start, make sure your environment meets these requirements:

Node.js 24.15+

The monorepo uses Node.js 24 LTS. Install via pnpm env use --global lts.

pnpm 11.5+

All workspace operations use pnpm. Install from pnpm.io.

Riven monorepo

A cloned and bootstrapped checkout of rivenmedia/riven-ts with dependencies installed.
1

Bootstrap the monorepo

If you have not already set up the monorepo, install dependencies and generate any required API schemas first:
pnpm install
turbo codegen:api-schemas
You should also have Docker running so the supporting services (Redis, PostgreSQL) can be started:
docker compose --profile services up -d
2

Run the plugin generator

From the repository root, run the Turborepo generator for plugins:
turbo gen plugin
The generator prompts you for a plugin name in kebab-case form. For example, entering my-service produces @repo/plugin-my-service under packages/plugin-my-service/.
? Plugin name (e.g., my-plugin): my-service
? This will create @repo/plugin-my-service in packages/plugin-my-service. Continue? (Y/n)
After confirming, the generator:
  • Creates the full plugin package under packages/plugin-my-service/
  • Adds "@repo/plugin-my-service": "workspace:^" as a dependency in both apps/riven and apps/wiki
  • Runs Prettier over all generated files
You can also pass the plugin name directly to skip the interactive prompt:
turbo gen plugin --args my-service
3

Understand the generated structure

The generator produces a complete, compiling plugin with every layer pre-wired:
packages/plugin-my-service/
├── lib/
│   ├── index.ts                            # Named `plugin` export — the entry point
│   ├── my-service-plugin.config.ts         # RivenPluginConfig: the plugin's Symbol name
│   ├── my-service-settings.schema.ts       # Zod schema for env-var configuration
│   ├── datasource/
│   │   ├── my-service.datasource.ts        # BaseDataSource subclass (HTTP client)
│   │   └── __tests__/
│   │       └── validate.spec.ts            # Validate endpoint test
│   └── schema/
│       ├── my-service.resolver.ts          # Primary GraphQL resolver
│       ├── my-service-settings.resolver.ts # Settings field resolver
│       └── types/
│           └── my-service-settings.type.ts # GraphQL ObjectType for settings
├── docs/
│   └── plugins/my-service/
│       ├── meta.json                       # Sidebar name + description for the wiki
│       └── settings.mdx                    # Settings reference page (hand-authored)
├── scripts/
│   └── generate-zod-docs.ts               # Generates settings docs from the Zod schema
├── package.json
├── tsconfig.json
├── turbo.jsonc
└── wiki.config.ts
Anything you add under docs/plugins/my-service/ is automatically picked up by the wiki — no registration required.
4

Define your plugin config

Open lib/my-service-plugin.config.ts. The generated file already reads the package name from package.json and wraps it in a Symbol — this is the canonical pattern used by every first-party plugin:
// lib/my-service-plugin.config.ts
import packageJson from '../package.json' with { type: 'json' };

import type { RivenPluginConfig } from '@repo/util-plugin-sdk';

export const pluginConfig = {
  name: Symbol(packageJson.name),
} satisfies RivenPluginConfig;
The Symbol value — "@repo/plugin-my-service" — is used as the BullMQ queue name prefix and as the GraphQL context injection key. Keep it stable across restarts by deriving it from the package name rather than creating an anonymous Symbol.
5

Define your settings schema

Open lib/my-service-settings.schema.ts. The generated file starts with an empty schema; add your fields here. Annotate every field with .describe() — the description is included in the generated settings documentation page.Here is a realistic example modelled after the TVDB plugin, which requires an API key:
// lib/my-service-settings.schema.ts
import z from 'zod';

export const MyServiceSettings = z.object({
  apiKey: z
    .string()
    .min(1, 'MyService API key is required')
    .describe('Your MyService API key'),
  url: z
    .url()
    .default('https://api.myservice.com')
    .describe('Base URL for the MyService API'),
  updateIntervalSeconds: z.coerce
    .number()
    .int()
    .nonnegative()
    .default(60)
    .describe('How often (in seconds) to re-check for new content'),
});

export type MyServiceSettings = z.infer<typeof MyServiceSettings>;
Users configure each field through an environment variable of the form:
RIVEN_PLUGIN_SETTING__REPO_PLUGIN_MY_SERVICE__apiKey="your-api-key-here"
RIVEN_PLUGIN_SETTING__REPO_PLUGIN_MY_SERVICE__url="https://api.myservice.com"
The environment variable prefix is derived from the package name: @repo/plugin-my-service becomes REPO_PLUGIN_MY_SERVICE. Riven strips these variables from process.env before importing any plugin, so they are invisible to all other plugins.
6

Implement your DataSource

Open lib/datasource/my-service.datasource.ts. The generated class extends BaseDataSource and provides a validate stub. Fill in baseURL, rateLimiterOptions, and any authentication logic in willSendRequest, then add your API methods.The example below is modelled after the production TVDB datasource, which injects a bearer token on every request:
// lib/datasource/my-service.datasource.ts
import { BaseDataSource } from '@repo/util-plugin-sdk';

import type { AugmentedRequest } from '@apollo/datasource-rest';
import type { RateLimiterOptions } from '@repo/util-plugin-sdk';
import type { MyServiceSettings } from '../my-service-settings.schema.ts';

class MyServiceAPIError extends Error {
  public override name = 'MyServiceAPIError';
}

export class MyServiceAPI extends BaseDataSource<MyServiceSettings> {
  public override baseURL = this.settings.url;
  public override serviceName = 'MyService';

  protected override rateLimiterOptions: RateLimiterOptions = {
    max: 50,       // 50 requests…
    duration: 1000, // …per second
  };

  protected override async willSendRequest(
    _path: string,
    requestOpts: AugmentedRequest,
  ) {
    requestOpts.headers['x-api-key'] = this.settings.apiKey;
  }

  // Called by the validator at startup — use the cheapest available endpoint
  public override async validate() {
    try {
      await this.get('health');
      return true;
    } catch {
      return false;
    }
  }

  // Example business method
  public async getItems(listId: string): Promise<Array<{ imdbId: string }>> {
    const response = await this.get<{ items: Array<{ imdb_id: string }> }>(
      `lists/${listId}/items`,
    );
    return response.items.map((item) => ({ imdbId: item.imdb_id }));
  }
}
Keep DataSources thin — they are API wrappers, not business logic. Transform raw API responses into the SDK’s common types (ExternalIds[], Movie, Show, etc.) right here, so resolvers and hooks deal only with typed domain objects.
7

Wire up the GraphQL resolver

The generated resolver in lib/schema/my-service.resolver.ts already uses @PluginDataSource() to inject the DataSource. This pattern is taken directly from the Comet plugin:
// lib/schema/my-service.resolver.ts
import { PluginDataSource } from '@repo/util-plugin-sdk';

import { Query, Resolver } from 'type-graphql';

import { pluginConfig } from '../my-service-plugin.config.ts';
import { MyServiceAPI } from '../datasource/my-service.datasource.ts';

@Resolver()
export class MyServiceResolver {
  @Query(() => Boolean)
  public async myServiceIsValid(
    @PluginDataSource(pluginConfig.name, MyServiceAPI) api: MyServiceAPI,
  ): Promise<boolean> {
    return api.validate();
  }
}
The settings resolver extends the global Settings GraphQL type so clients can query available setting keys:
// lib/schema/my-service-settings.resolver.ts
import { Settings } from '@repo/util-plugin-sdk';

import { FieldResolver, Resolver } from 'type-graphql';

import { MyServiceSettings } from './types/my-service-settings.type.ts';

@Resolver(() => Settings)
export class MyServiceSettingsResolver {
  @FieldResolver(() => MyServiceSettings)
  public myService(): MyServiceSettings {
    return {
      apiKey: 'my-service-api-key',
      url: 'my-service-url',
    };
  }
}
8

Assemble the plugin manifest

Open lib/index.ts. The generated file exports a plugin constant that combines everything. Add event hooks relevant to your plugin type. This example adds a scrape hook — following the same pattern as plugin-comet:
// lib/index.ts
import packageJson from '../package.json' with { type: 'json' };
import { MyServiceAPI } from './datasource/my-service.datasource.ts';
import { pluginConfig } from './my-service-plugin.config.ts';
import { MyServiceSettings } from './my-service-settings.schema.ts';
import { MyServiceSettingsResolver } from './schema/my-service-settings.resolver.ts';
import { MyServiceResolver } from './schema/my-service.resolver.ts';

import type { RivenPlugin } from '@repo/util-plugin-sdk';

export const plugin: RivenPlugin = {
  name: pluginConfig.name,
  version: packageJson.version,
  dataSources: [MyServiceAPI],
  resolvers: [MyServiceResolver, MyServiceSettingsResolver],
  hooks: {
    'riven.media-item.scrape.requested': async ({ dataSources, event }) => {
      const api = dataSources.get(MyServiceAPI);
      const results = await api.scrape(event);

      return {
        id: event.item.id,
        results,
      };
    },
  },
  settingsSchema: MyServiceSettings,
  async validator({ dataSources }) {
    return dataSources.get(MyServiceAPI).validate();
  },
};
The hooks object may be empty ({}) — you are not required to subscribe to any events. The resolvers array must contain at least one entry; the generated resolver satisfies this requirement out of the box.
9

Add the plugin to apps/riven

The generator already added "@repo/plugin-my-service": "workspace:^" to apps/riven/package.json and apps/wiki/package.json. You do not need to edit those files manually.If for some reason you created the package by hand, add the dependency yourself:
pnpm add @repo/plugin-my-service --workspace --filter @repo/riven
After adding or modifying the dependency, regenerate the lockfile:
pnpm install
10

Run the test suite

The generator also creates a validate.spec.ts test in lib/datasource/__tests__/ using the @repo/util-plugin-testing context helpers and MSW for mocking HTTP responses. Run only your plugin’s tests during development:
turbo test --filter=@repo/plugin-my-service
Or watch for changes:
turbo test:watch --filter=@repo/plugin-my-service
To run the full monorepo test suite (requires a local redis-server):
turbo test --continue
11

Start Riven in development mode

With services running and your .env.riven file configured, start the application:
turbo @repo/riven#dev
Check for startup logs confirming your plugin was registered and validated. If the validator returns false, the plugin appears in the logs as invalid — it remains loaded but receives no events. Use the Bull Board dashboard at http://localhost:4000 to inspect queue activity.
During development it is helpful to enable these flags in .env.riven so every restart begins from a clean state:
RIVEN_SETTING__unsafeWipeRedisOnStartup=true
RIVEN_SETTING__unsafeWipeDatabaseOnStartup=true

Real-world plugin examples

Two first-party plugins make good reference implementations for different plugin roles:

plugin-comet

A scraper plugin. Implements riven.media-item.scrape.requested to query the Comet torrent indexer API and return a map of info-hash → stream-title strings. No API key required; only a configurable base URL via CometSettings.

plugin-tvdb

A metadata provider plugin. Implements riven.media-item.index.requested.show to fetch full series metadata from TVDB v4 (with automatic JWT token refresh) and episode data from TVMaze for timezone information. Uses two DataSources in a single plugin.

Comet plugin config (scraper)

// packages/plugin-comet/lib/index.ts
export const plugin: RivenPlugin = {
  name: pluginConfig.name,
  version: packageJson.version,
  dataSources: [CometAPI],
  resolvers: [CometResolver, CometSettingsResolver],
  hooks: {
    'riven.media-item.scrape.requested': async ({ dataSources, event }) => {
      const api = dataSources.get(CometAPI);
      const results = await api.scrape(event);

      return {
        id: event.item.id,
        results,
      };
    },
  },
  settingsSchema: CometSettings,
  async validator() {
    return Promise.resolve(true);
  },
};

TVDB plugin config (metadata provider with two DataSources)

// packages/plugin-tvdb/lib/index.ts
export const plugin: RivenPlugin = {
  name: pluginConfig.name,
  version: packageJson.version,
  dataSources: [TvdbAPI, TvMazeAPI],
  resolvers: [TvdbResolver, TvdbSettingsResolver],
  hooks: {
    'riven.media-item.index.requested.show': indexTVDBMediaItem,
  },
  settingsSchema: TvdbSettings,
  async validator({ dataSources }) {
    return dataSources.get(TvdbAPI).validate();
  },
};

Next steps

Building a DataSource

Rate limiting, caching options, willSendRequest patterns, and error handling in depth

GraphQL Resolvers

Query and mutation resolvers, PluginContext injection, and settings resolvers

Event Hooks

Full event catalogue, handler signatures, and return type requirements

Settings

Zod schema patterns, environment variable naming, and the generated docs pipeline

Build docs developers (and LLMs) love