Skip to content

Writing a connector ​

You can create and share your own connectors to pull custom sources into OpenFeed.

Basics ​

An OpenFeed connector is a TypeScript module that exports a BackendFeedPlugin object with the following shape:

typescript
import type {
  BackendFeedPlugin,
  FetchFn,
  PluginAS2Object,
  PluginContext,
} from "openfeed/connectors/types";

const myConnector: BackendFeedPlugin = {
  name: "my-connector",
  icon: `<svg>...</svg>`, // optional SVG icon string

  // Return true if this connector can handle the given URL
  canHandle: (url: string) => url.includes("mysite.com"),

  // Fetch and return items for the given source URL
  listItems: async (
    sourceUrl: string,
    fetchFn: FetchFn,
    context: PluginContext,
    options?: Record<string, unknown>,
  ): Promise<readonly PluginAS2Object[]> => {
    const response = await fetchFn(sourceUrl);
    const data = await response.json();
    return data.items.map((item) => ({
      type: "Article",
      name: item.title,
      url: item.url,
      published: new Date(item.date),
      content: item.body,
      mediaType: "text/html",
    }));
  },
};

export default myConnector;

Each item must include the required fields type and url. The type field determines how the content is displayed:

TypeDescriptionCommon fields
ArticleLong-form content with a headline (RSS, blog posts, news)name, content
NoteShort-form text without a standalone title (social posts)content
VideoVideo content with embed linkurl, attachment
AudioAudio content with enclosure URLurl, attachment
EventCalendar eventname, published
PageGeneric web page best shown in an iframeurl

Error handling ​

Throw FeedError instead of plain Error so OpenFeed can record a structured error code alongside the message. This allows the UI to group errors by type and makes run history easier to interpret.

Import FeedError from the types module:

typescript
import { FeedError } from "openfeed/connectors/types";
CodeWhen to use
source_not_foundThe source URL returned a 404 or the resource could not be located
item_not_foundThe source was reachable but a specific item URL was missing
parse_errorThe source was reachable but the content couldn't be parsed
invalid_configThe user has configured options incorrectly (bad URL shape, wrong option type, unsupported URL)
missing_credentialA required environment variable or credential option is absent
auth_errorA credential is present but authentication failed (401/403)
rate_limitedThe source returned a 429 or equivalent rate-limit response
network_errorA network-level failure occurred (timeout, DNS failure, connection refused, unexpected HTTP status)
unknownCatch-all for unexpected errors — used automatically for plain Error throws
typescript
const apiKey = process.env["MY_API_KEY"];
if (!apiKey) {
  throw new FeedError("MY_API_KEY is not set.", "missing_credential");
}

const response = await fetchFn(sourceUrl);

if (response.status === 404) {
  throw new FeedError(`Source not found: ${sourceUrl}`, "source_not_found");
}

if (response.status === 401 || response.status === 403) {
  throw new FeedError(
    `Authentication failed: HTTP ${response.status}`,
    "auth_error",
  );
}

if (response.status === 429) {
  throw new FeedError("Rate limited by mysite.com", "rate_limited");
}

if (!response.ok) {
  throw new FeedError(`Unexpected HTTP ${response.status}`, "network_error");
}

If a connector throws a plain Error, OpenFeed records it with code unknown. Switching to FeedError only adds the structured code.

Local testing ​

Write tests using Vitest. Inject a mock fetchFn — never hit the network in tests:

typescript
import { describe, it, expect, vi } from "vitest";
import myConnector from "./index";

describe("myConnector", () => {
  it("handles mysite.com URLs", () => {
    expect(myConnector.canHandle("https://mysite.com/feed")).toBe(true);
    expect(myConnector.canHandle("https://other.com")).toBe(false);
  });

  it("returns items", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          items: [
            {
              title: "Test",
              url: "https://mysite.com/1",
              date: "2024-01-01",
              body: "Hello",
            },
          ],
        }),
    });
    const context = { sourceName: "Test", sourceUrl: "https://mysite.com" };
    const items = await myConnector.listItems(
      "https://mysite.com",
      mockFetch,
      context,
    );
    expect(items).toHaveLength(1);
    expect(items[0].name).toBe("Test");
  });
});

Loading your connector ​

Local file ​

Point to your connector file in openfeed.yaml (relative to the config file):

yaml
connectors:
  - ./my-connector.js

feeds:
  - name: Main
    sources:
      - name: My Source
        url: https://mysite.com
        connector: my-connector

npm package ​

Publish your connector and install it on the server, then reference it by package name:

bash
npm install openfeed-connector-mysite
yaml
connectors:
  - openfeed-connector-mysite

feeds:
  - name: Main
    sources:
      - name: My Source
        url: https://mysite.com
        connector: mysite

Name the npm package with the openfeed-connector- prefix so others can discover it. We also welcome PRs to include connectors in the built-in connector list.