---
title: "Custom Providers"
description: "Extend the base class, answer the listing call, optionally serve bodies, and plug into the same archive."
canonical_url: "https://archives.agntn.dev/guide/custom"
---
# Custom Providers

> Extend the base class, answer the listing call, optionally serve bodies, and plug into the same archive.

## The contract

```ts
interface ArchiveProvider {
  name: string;
  slug?: string;
  readonly options?: ArchiveOptions;
  cacheKey?: (options?: ArchiveOptions) => string | undefined;
  snapshots: (domain: string, options?: ArchiveOptions) => Promise<ArchiveResponse>;
  content?: (url: string, options?: ArchiveContentOptions) => Promise<ArchiveContentResponse>;
}
```

`snapshots` is the only required method. `content` is optional: a provider that serves captures only through its own UI leaves it out, and the aggregator reports the missing method the same way it reports an explicit unsupported response.

## Extend the base class

```ts [my-archive.ts]
import { BaseProvider, type ArchiveOptions, type ArchiveResponse } from "@agntn/archives";

interface MyArchiveOptions extends ArchiveOptions {
  collection?: string;
}

export class MyArchiveProvider extends BaseProvider<MyArchiveOptions> {
  readonly name = "My Archive";
  readonly slug = "my-archive";

  /** Options that change the result set belong in the cache key. */
  override cacheKey(options?: Readonly<MyArchiveOptions>): string | undefined {
    const collection = options?.collection ?? this.options.collection;
    return collection ? `collection=${collection}` : undefined;
  }

  async snapshots(domain: string, requestOptions?: Readonly<MyArchiveOptions>): Promise<ArchiveResponse> {
    const options = await this.resolveOptions(requestOptions);
    try {
      const rows = await fetchMyIndex(domain, options); // your HTTP call, honouring options.timeout and options.signal
      return {
        success: true,
        pages: rows.map((row) => ({
          url: row.original,
          timestamp: new Date(row.captured).toISOString(),
          snapshot: `https://my-archive.example/${row.id}`,
          _meta: { provider: "my-archive", status: row.status },
        })),
        _meta: { source: "my-archive", provider: "my-archive" },
      };
    } catch (error) {
      return {
        success: false,
        pages: [],
        error: error instanceof Error ? error.message : String(error),
        _meta: { source: "my-archive", provider: "my-archive" },
      };
    }
  }
}
```

`resolveOptions` merges the factory options, the options of the call and the resolved config, in that order of precedence. `this.options` holds what the constructor received.

Three rules the built-in providers follow, and a review would ask of yours:

- **Timestamps are ISO 8601.** Convert at the boundary; the merged listing sorts by `timestamp` and the window filter parses it.
- **_meta.provider on every page.** A merged listing has no other way to say where a page came from.
- **A missing capability is a response, not an exception.** Return `{ success: false, unsupported: true, unsupportedReason }` for a call your archive cannot make, so `providers.all()` can list it under `unsupportedProviders` instead of failing.

## Serve bodies

Add `content()` when the archive can replay original responses:

```ts
async content(url: string, requestOptions?: Readonly<ArchiveContentOptions>): Promise<ArchiveContentResponse> {
  const options = await this.resolveContentOptions(requestOptions);
  const capture = await pickCapture(url, options.timestamp); // newest at or before, else closest after
  const body = await readBody(capture, options.maxBytes);
  return {
    success: true,
    content: {
      url: capture.original,
      timestamp: capture.iso,
      snapshot: capture.rawUrl,
      content: body.text,
      mime: body.mime,
      bytes: body.bytes,
      truncated: body.truncated,
      _meta: { provider: "my-archive" },
    },
    _meta: { source: "my-archive", provider: "my-archive" },
  };
}
```

Read from an endpoint that returns the original response, never a page that wraps it. The diff helper compares whatever `content` says, and a replay banner would show up as a change.

## Use it

```ts
import { createArchive, providers } from "@agntn/archives";
import { MyArchiveProvider } from "./my-archive";

const archive = createArchive(new MyArchiveProvider({ collection: "news" }));
await archive.use(providers.wayback());

const response = await archive.snapshots("example.com");
```

An instance and a lazy `Promise<ArchiveProvider>` are both accepted, so a custom provider sits next to the built-in ones without a wrapper.

## Where the built-ins live

`src/providers/` holds one file per archive; `wayback.ts` is the template. The shared helpers in `src/utils/` map CDX rows, select captures, read playback and WARC bodies, and build the response objects, so a new built-in provider is mostly the index mapping. `src/providers/index.ts` registers the lazy factory.


## Sitemap

See the full [sitemap](https://archives.agntn.dev/sitemap.md) for all pages.
